汇编基础

汇编基础

高级语言接口

大多数程序员不会用汇编语言写大的程序,因为这相当的花费时间。反之,高级语言会隐藏一些细节,开发效率更高。汇编语言广泛用于配置硬件驱动器,以优化程序速度和代码量

通用规范

  • 调用规范(call convention)是指调用过程的底层细节。下面列出需要考虑的细节信息:

    • 调用过程需要保存哪些寄存器
    • 传递参数的方法:用寄存器,用堆栈,共享内存或其他方法
    • 主调程序调用过程时参数传递的顺序
    • 参数传值还是传引用
    • 过程调用后,如何恢复堆栈指针
    • 函数如何向主调程序返回结果
  • 命名规范与外部标识符
    当从其他语言程序中调用汇编过程时,外部标识符必须与命名规范兼容。外部标识符(external identifier)是放在模块目标文件中的名称,链接器使得这些名称能被其他程序模块使用。

  • 段名称
    汇编语言过程与高级语言程序连接时,段名称必须是兼容的。本章使用的简化段指令都与Microsoft C++编译器生成的段名称兼容

  • 内存模式
    主调程序与被调程过程使用的内存模式必须相同

Q:实地址模式下可以选择那些内存模式?
K:.model伪指令确定若干重要的程序特性:内存模式类型,过程命名模式,参数传递规则
A:有微模式,小模式,中模式,大模式,紧凑模式,巨模式,平坦模式,值得一提的是:平坦模式也是保护模式,代码与数据使用32位偏移量。所有的数据和代码(包括系统资源)都在一个32位段内。

Q:使用STDCALL语言说明符的汇编语言过程可以与C++程序链接
A:F,STDCALL用于Windows的系统函数调用,C语言说明符才是可以与C/C++链接的过程
K:需要注意,C说明符将从堆栈中移除参数的任务交给了主调方,C语言说明符在外部过程名的前面添加前导下滑线,如_AddTwo

内嵌汇编代码

Q:内嵌汇编代码与内嵌C++过程有什么不同之处?
A:内嵌汇编代码是指的直接插入高级语言程序的汇编源代码,而C++内嵌限定符则要求C++汇编器直接把函数体插入程序的编译代码,以便清除函数调用和返回时所耗费的时间。

Q:与使用外部汇编过程相比,内嵌汇编代码有什么优势?
A:简单,因为不需要考虑外部链接,命名以及参数传递协议

Q:请至少给出两种在内嵌汇编代码中添加注释的方法
A:建议不要使用汇编风格的注释,选择使用

/ xxxx /
/* xxxxx */

Q:内嵌语言是否可以引用__asm模块外的代码
A:可以
K:
编写内嵌汇编代码时允许:

  • 使用x86指令集的大多数指令
  • 使用寄存器名作为操作数
  • 通过名字引用函数参数
  • 引用asm块外定义的代码标号和变量。(这点很重要,因为局部变量必须在asm块的外面)
  • 在语句中使用PTR,EVEN(使下一个变量或指令开始于偶数字节地址),ALIGN(双字对齐)
    不允许:
  • 使用数据定义伪指令,如DB(BYTE)和DW(WORD)
  • 使用汇编运算符(PTR除外)
  • 使用STRUCT,RECORD,WIDTH,MASK
  • 使用宏伪指令,以及宏运算符(<>,!,&,%)

虽然不能使用OFFSET运算符,但可以用lea指令获取变量的偏移值
lea esi, buffer

实例:

使用汇编加密

encode.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <fstream>
#include "translat.h"
using namespace std;

int main(int argcount, char *args[])
{
if (argcount < 3) {
cout << "Usage:encode infile outfile" << endl;
return -1;
}
const int BUFSIZE = 2000;
char buffer[BUFSIZE];
unsigned int count; //字符计数

unsigned char encryptCode;
cout << "Encryption code[0-255]?";
cin >> encryptCode;

ifstream infile(args[1], ios::binary);
ofstream outfile(args[2], ios::binary);
cout << "Reading " << args[1] << " and creating "
<< args[2] << endl;

while (!infile.eof()){
infile.read(buffer, BUFSIZE);
count = infile.gcount();
TranslateBuffer(buffer, count, encryptCode);
outfile.write(buffer, count);
}
return 0;
}

嵌入的汇编语言写在头文件里,加密方法就是让字符每一位与输入的数字异或

translat.h
1
2
3
4
5
6
7
8
9
10
11
12
13
void TranslateBuffer(char *buf, 
unsigned count,
unsigned char eChar) {
__asm {
mov esi, buf
mov ecx, count
mov al, eChar
L1:
xor [esi], al
inc esi
loop L1
} // asm
}

我们在命令行里运行

encode infile.txt encoded.txt

我们反汇编看到一些东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
000D7D80  push        ebp  
000D7D81 mov ebp,esp
000D7D83 sub esp,0C0h
000D7D89 push ebx
000D7D8A push esi
000D7D8B push edi
;内嵌代码开始
000D7D8C lea edi,[ebp-0C0h]
000D7D92 mov ecx,30h
000D7D97 mov eax,0CCCCCCCCh
000D7D9C rep stos dword ptr es:[edi]
000D7D9E mov esi,dword ptr [buf]
000D7DA1 mov ecx,dword ptr [count]
000D7DA4 mov al,byte ptr [eChar]
000D7DA7 xor byte ptr [esi],al
000D7DA9 inc esi
000D7DAA loop L1 (0D7DA7h)
;这里汇编结束
000D7DAC pop edi
000D7DAD pop esi
000D7DAE pop ebx
000D7DAF add esp,0C0h
000D7DB5 cmp ebp,esp
000D7DB7 call __RTC_CheckEsp (0D15AFh)
000D7DBC mov esp,ebp
000D7DBE pop ebp
000D7DBF ret

编译器自动插入了一些语句用于设置EBP以及保存标志寄存器集合,集合内的寄存器不论是否被修改,总是会被保存

32位汇编程序与C/C++的链接

要想调用,需要在汇编源码中.MODEL伪指令中指定调用C的规范,而且要为每个调用外部的过程创建一个原型

1
2
3
4
.586
.model flat,C
IndexOf PROTO,
srchVal:DWORD, arrayPtr:DWORD, count:DWORD

在C程序中,声明外部汇编过程时要使用extern限定符,如果过程会被C++调用,则要添加“C”限定符,防止C++的名称修饰

1
2
extern long IndexOf(long n, long array[], unsigned count);
extern "C" long IndexOf(long n, long array[], unsigned count);

名称修饰(name decoration)就是一种标志C++编译技术,通过添加字符来修改函数名,添加的字符指明了每个函数参数的确切类型
详细的配置可以看我们的实验

调用C和C++的函数

函数原型

可以编写程序来调用C和C++函数,这样做的理由至少有:

  • C和C++有丰富的输入输出库,因此有更大的灵活性,处理浮点数时,这相当有用
  • 两种语言都有丰富的数学库
    需要注意:调用标准C库或C++库,必须从C或C++的main()过程启动程序,以便运行库初始化代码
    汇编语言调用的C++函数必须使用“C”和关键字extern,推荐不是修改每个函数的定义,而是把多个函数原型放在一个块中显得更容易
1
2
3
4
extern "C"{
int askForInteger();
int showInt(int value, unsigned outWidth)
}

汇编语言模块

如果汇编语言模块要调用Irvine32链接库,就要使用.model flat, stdcall,但这和c的调用规范不符(要用c规范),这时声明原型必须给PROTO伪指令加上C限定符

1
2
3
INCLUDE Irvine32.inc
askForInteger PROTO C
showInt PROTO C, value:SDWORD, outWidth:DWORD

当然如果不调用Irvine库,则全局声明为C规范即可.model flat, C,PROTO就不用再添加C限定符

仍需要知道,Microsoft Visual C++函数怎样返回数值:

  • bool和char的值用AL返回
  • short,int值用AX返回
  • int和long int值用EAX返回
  • 指针用EAX返回
  • float,double,long double的值分别以4,8,10字节值压入浮点堆栈

调用C库(Standard Library)函数

printf

C的原型

1
2
3
int printf(
const char *format [, argument]...
)

汇编的等效原型

1
printf PROTO C,format:PTR BYTE, args:VARARG

可变长度参数类型为vararg

如何调用printf?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
;显示双精度
.data
double1 REAL8 1234567.890123
formatStr BYTE "%3.f", 0dh, 0ah, 0
.code
INVOKE printf, ADDR formatStr, double1 ;结果=1234567.890
;多参数
TAB=9
.data
formatTwo BYTE "%.2f", TAB, "%.3f", 0dh, 0ah, 0
val1 REAL8 111.111
val2 REAL8 456.789
.code
INVOKE printf, ADDR formatTwo, val1, val2

需要注意的是:格式化字符串不是插入转义字符,如\n,必须插入ASCII字符(0dh,0ah)

写在后面

至此,基础基本完成,庆祝一下自己坚持看完了这本400+的书,习题会补上。

Author

Ctwo

Posted on

2019-08-10

Updated on

2020-10-25

Licensed under

Comments