《天书夜读:从汇编语言到windows内核编程》一 汇编指令与C语言

时间:2021-08-18 14:10:38

1、 Debug模式下,VC++6.0下断点运行,按CTRL+F11可查看汇编代码;另外可以用cl /c /FAs YourCppFile.cpp命令行在同目录生成YourCppFile.asm汇编文件。

 

2、 Push将32位操作数压入堆栈,esp指向栈顶,故esp减去4(字节=32位,在64位机器上则是8)。记住:esp为栈顶指针,堆栈越高,这个值越小。由于Intel处理器采用小端存储模式,故当PUSH一个寄存器(如EBP)的值0012FFB4时,先存低字节再存高字节,即堆栈从栈顶向下应该是B4FF1200,当然,采用LONG型HEX解析内存时显示的数值将不变,仍是0012FFB4。

 

3、 CALL的本质相当于PUSH+JMP(压入CALL语句下一条指令地址,然后跳转至CALL指定的函数入口地址),RET的本质相当于POP+JMP(弹出CALL语句下一条指令地址,然后跳转该地址)。

 

4、 对堆栈的操作指令除了PUSH、POP、CALL、RET以外,还可以是ADD,SUB。在C语言中局部变量是保持在栈里,可以用esp – 4*4的方式在堆栈中分配4个4字节长的整形空间,而不用PUSH4次。

 

5、 用XOR EAX,EAX来取代MOV EAX,0,这样来实现清零的好处是:速度更快,占用字节更少。

 

6、 LEA取址运算符,但如下语句:LEA EDI,[EBP - 0CCH]与MOV EDI,EBP - 0CCH在语义上是等同的([X]表示存储器X中的内容),但是后者会导致语法错误,原因是MOV不支持后一个操作数为寄存器减去一个数字的形式

 

7、 C语言函数调用默认方式(_cdcall):调用方把参数反序(从右到左)压入堆栈,被调用方把堆栈复原(获取参数)。这些参数须对齐到机器字长,32、64位CPU分别对齐到4、8个字节。

 

8、 右3可知,CALL和RET并不是函数存在的绝对证据,我们可以自行操作堆栈然后使用JMP绝对跳转实现函数调用。由于高级语言对函数调用的规则各不一样,由此产生了调用约定,在windows上有:Pascal方式、WINAPI方式(_stdcall)、C方式(_cdcall)。

 

9、 Pascal函数调用方式基本用在16位函数库中,现在基本不用。

 

10、_stdcall函数调用方式规则(Windows API):参数从右到左入栈;被调用的函数在返回前自行清理堆栈(可变参数函数调用除外)。

 

11、_cdcall函数调用方式规则(C编译器默认):参数从右到左入栈;函数返回后,调用者负责清理堆栈,由此通常生成较大可执行文件

 

12、不管何种调用方式,返回值都放入EAX寄存器中。

 

13、_cdcall函数:

1 int MyFunction(int a,int b)  
2 {
3 return a+b;
4 }

汇编后代码为:

 1 int MyFunction(int a,int b)  
2 {
3 0040D430 push ebp ;1)
4 0040D431 move ebp,esp ;2)
5 0040D433 sub esp,40h ;3)
6 0040D436 push ebx
7 0040D437 push esi
8 0040D438 push edi ;4)
9 0040D439 lea edi,[ebp-40h]
10 0040D43C mov ecx,10h
11 0040D441 mov eax,0CCCCCCCCh
12 0040D446 rep stos dword ptr [edi] ;5)
13 return a+b;
14 0040D448 mov eax,dword ptr [ebp+8]
15 0040D44B add eax,dword ptr [ebp+0Ch] ;6)
16 }
17 0040D44E pop edi
18 0040D44F pop esi
19 0040D450 pop ebx ;7)
20 0040D451 mov esp,ebp
21 0040D453 pop ebp ;8)
22 0040D454 ret

它所做的事情有:

1) 保存EBP(基址寄存器)。保存之前的ESP值,在返回时恢复,使函数对堆栈能够实现正确操作。

2) 保存ESP到EBP,此时两者相等,都是函数调用以后的栈基址(栈顶)。

3) 在栈中腾出40H的字节区域用来保存局部变量。大小是可变的,由编译器自动分配。

4) 保存ebx,esi,edi到堆栈,函数调用完后恢复。

5) 初始化局部变量区域,循环10H次,每次赋值4个字节(共10H * 4 = 40H字节,与上面分配的字节区域刚好对应)为0CCCCCCCCH,这个值实际上是INT3的机器码,是一个断点中断指令,因局部变量区域不可能被执行,如果执行了则报错。

6) 第一个参数获取位置为ebp + 双倍CPU字长,这里是ebp + 8,后面的依据类型进行偏移。需要说明一点的是,调用者在调用前将参数倒序压入堆栈,所有参数压入以后,在执行CALL指令时,它会自动将CALL指令下面的一条指令地址压入堆栈;此外,进入调用的函数以后,第一件事就是压入EBP到堆栈。由此可以看出,函数当前栈顶(ESP指向位置)与最后一个压入栈的参数(参数列表的第一个参数)相隔了两个CPU字长。这里是4 * 2 =8个字节,故为ebp + 8。

7) 恢复ebx,esi,edi。这里是与入口对应的现场恢复,没什么好说的

8) EBP是被调用者栈顶指针,其内存单元的值是调用者栈顶指针。所以,这里一方面是使ESP指向被调用者的栈顶(也是调用者的栈底),另一方面是恢复调用者的栈基址。

9) RET执行函数返回,此时,ESP自动加上一个CPU字长,指向最后一个被压入的参数位置。

10)  函数的返回值在EAX中,这里不需要额外操作,如果结果不是在EAX,则在返回前一定有MOV操作(或其等同效果的操作)

 

14、对上面的程序执行如下调用:

1 int main(void)  
2 {
3 MyFunction(1,2);
4 return 0;
5 }

其汇编代码为:

 1 int main(void)  
2 {
3 0040D3F0 push ebp ;a)
4 0040D3F1 mov ebp,esp
5 0040D3F3 sub esp,40h ;b)
6 0040D3F6 push ebx ;c)
7 0040D3F7 push esi ;d)
8 0040D3F8 push edi ;e)
9 0040D3F9 lea edi,[ebp-40h]
10 0040D3FC mov ecx,10h
11 0040D401 mov eax,0CCCCCCCCh
12 0040D406 rep stos dword ptr [edi]
13 MyFunction(1,2);
14 0040D408 push 2 ;f)
15 0040D40A push 1 ;g)
16 0040D40C call @ILT+5(MyFunction) (0040100a) ;h)
17 0040D411 add esp,8
18 return 0;
19 0040D414 xor eax,eax
20 }
21 0040D416 pop edi
22 0040D417 pop esi
23 0040D418 pop ebx
24 0040D419 add esp,40h
25 0040D41C cmp ebp,esp
26 0040D41E call __chkesp (0040d460)
27 0040D423 mov esp,ebp
28 0040D425 pop ebp
29 0040D426 ret

对以上代码执行时的内存单元数据情况如下表:

操作
堆栈地址 HEX数据 描述 EBP寄存器
     
4) 0012FED8 0012FF80 MyFunction函数压入edi 0012FF24
4) 0012FEDC 01F8BCE8 MyFunction函数压入esi 0012FF24
4) 0012FEE0 7FFDF000 MyFunction函数压入ebx 0012FF24
3)、5)

0012FEE4

0012FF20

CCCCCCCC

CCCCCCCC

MyFunction函数压入预留局部变量存储区,全部初始化为0xCCCCCCCCH 0012FF24
1)、2) 0012FF24 0012FF80 MyFunction函数压入ebp 0012FF24
h) 0012FF28 0040D411 mian函数CALL调用,压入返回地址 0012FF80
g) 0012FF2C 00000001 参数1入栈 0012FF80
d) 0012FF30 00000002 参数2入栈 0012FF80
e) 0012FF34 01DD0000 main函数压入edi 0012FF80
d) 0012FF38 01F8BCE8 main函数压入esi 0012FF80
c) 0012FF3C 7FFDF000 main函数压入ebx 0012FF80
b)

0012FF40

0012FF7C

CCCCCCCC

CCCCCCCC

main函数压入预留局部变量存储区,全部初始化为0xCCCCCCCCH 0012FF80
a) 0012FF80 0012FFC0 main函数压入ebp 0012FF80

 

函数地用过程中的操作由下往上看,注意CALL与RET指令执行时的堆栈变化。其中红色和蓝色为EBP寄存器变化情况,灰绿色为参数压栈。

 

15、 需要注意的是,以上的汇编代码来自于VC++6.0编译器,在自行写汇编或者嵌入汇编时要稍做改变。比如说CALL指令直接写为call MyFunction