20:C语言与汇编
20.1:调用约定之汇编
x86调用约定:
- cdecl:参数从右往左依次入栈,调用者栈平衡(C语言缺省的调用约定,支持可变参数)
- stdcall:参数从右往左依次入栈,被调用者栈平衡
-
fastcall:前两个参数ecx、edx,后面参数从右往左依次入栈,被调用者栈平衡(x86)
当调用fun函数开始时,三者的作用。
- eip寄存器:存储的是CPU下次要执行的指令的地址。也就是调用完fun函数后,让CPU知道应该执行main函数中的printf("函数调用结束")语句了。
- ebp寄存器:存储的是是栈的栈底指针,通常叫栈基址,这个是一开始进行fun()函数调用之前,由esp传递给ebp的。(在函数调用前你可以这么理解:esp存储的是栈顶地址,也是栈底地址。)
- esp寄存器:存储的是在调用函数fun()之后,栈的栈顶。并且始终指向栈顶。
20.1.1:cdecl
语法:
- int func (int x ,int y) //默认的C调用约定
- int __cdecl func (int x, int y)//明确指出C调用约定
//由于每次函数调用都要由编译器产生还原栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是__cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf()和Windows的API wsprintf()就是__cdecl调用方式。
//由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,就可以使用不定参数了。
int __cdecl func2(int x, int y) {
return x+y;
}
//被调用者汇编代码:
int __cdecl func2(int x, int y)//采用cdecl调用约定
{
0042D680 push ebp //ebp入栈)(ebp栈基址)
0042D681 mov ebp,esp //ebp指向esp(esp栈顶)
0042D683 sub esp,0C0h//esp增长0xC0个空间
0042D689 push ebx //ebx,esi,edi寄存器入栈,这三个寄存器操作系统没有备份
0042D68A push esi
0042D68B push edi
0042D68C lea edi,[ebp-0C0h]// 将edi赋值为ebp-0C0h(lea取得偏移地址) ;[ebp-0C0h]即栈顶,将cccccccc拷贝到栈的局部空间中;INT3指令的机器码为CC,即CC是断点指令(作用:一旦程序出了问题,eip指针,如果eip中的指针指向栈上,开始执行就执行int 3h)
0042D692 mov ecx,30h//注意到30h * 4 = 0C0h
0042D697 mov eax,0CCCCCCCCh//0CCCCCCCCh为系统中断int 3h;
0040042D69C rep stos dword ptr es:[edi]//拷贝开始
return x+y;
0042D69E mov eax,dword ptr [x]
0042D6A1 add eax,dword ptr [y] //eax最通用的用法放函数返回值
}
0042D6A4 pop edi//edi,esi,ebx出栈
0042D6A5 pop esi
0042D6A6 pop ebx
0042D6A7 mov esp,ebp//esp收缩空间
0042D6A9 pop ebp//ebp出栈
00000042D6AA ret//被调用者直接返回,不用恢复栈平衡,由调用者负责
//调用者代码:
func2(1, 2);//采用cdecl调用约定,参数从右往左依次入栈,调用者负责栈平衡
//0042D737 push 2//参数从右往左依次入栈,2入栈
//0042D739 push 1//参数从右往左依次入栈,1入栈
//0042D73B call func2 (42B3FCh)
//0042D740 add esp, 8 //调用者负责栈平衡,esp+8,等于2个入栈参数的长度
20.1.2:stdcall
语法:
- int __stdcall func(int x, int y)
//函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。
int __stdcall func1(int x, int y) {
return x+y;
}
//被调用者代码:
int __stdcall func1(int x, int y)//采用stdcall
{
42D640 push ebp
0042D641 mov ebp,esp
0042D643 sub esp,0C0h
0042D649 push ebx
0042D64A push esi
0042D64B push edi
0042D64C lea edi,[ebp-0C0h]
0042D652 mov ecx,30h
0042D657 mov eax,0CCCCCCCCh
0042D65C rep stos dword ptr es:[edi]
return x+y;
0042D65E mov eax,dword ptr [x]
0042D661 add eax,dword ptr [y]
}
0042D664 pop edi
0042D665 pop esi
0042D666 pop ebx
0042D667 mov esp,ebp //ebp(调用前的栈顶)放入esp中,然后出栈,恢复老ebp
0042D669 pop ebp
0042D66A ret 8 //被调用者负责栈平衡,ret 8,esp += 8;
//调用者代码
func1(1, 2); //采用stdcall,参数从右往左依次入栈,被调用者负责栈平衡
//0042D72E push 2 //参数从右往左依次入栈,2入栈
//0042D730 push 1 //参数从右往左依次入栈,1入栈
//0042D732 call func1 (42B6F4h)
20.1.3:fastcall
语法:
- int fastcall func (int x, int y)
//函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。以fastcall声明执行的函数,具有较快的执行速度,因为部分参数通过寄存器来进行传递的。
int __fastcall func3(int x, int y, int z) {
return x+y+z;
}
//被调用者汇编代码:
int __fastcall func3(int x, int y, int z)//采用fastcall调用约定
{
0042D6C0 push ebp
0042D6C1 mov ebp,esp
0042D6C3 sub esp,0D8h
0042D6C9 push ebx
0042D6CA push esi
0042D6CB push edi
0042D6CC push ecx //存放参数入栈
0042D6CD lea edi,[ebp-0D8h]
0042D6D3 mov ecx,36h
0042D6D8 mov eax,0CCCCCCCCh
0042D6DD rep stos dword ptr es:[edi]
0042D6DF pop ecx
0042D6E0 mov dword ptr [ebp-14h],edx //前2个参数放在了ecx和edx中
0040042D6E3 mov dword ptr [ebp-8],ecx//前2个参数放在了ecx和edx中
return x+y+z;
0042D6E6 mov eax,dword ptr [x]
0042D6E9 add eax,dword ptr [y]
0042D6EC add eax,dword ptr [z]
}
0042D6EF pop edi
0042D6F0 pop esi
0042D6F1 pop ebx
0042D6F2 mov esp,ebp
0042D6F4 pop ebp
0040042D6F5 ret 4 //第3个参数占4个字节,从栈上传递,所以栈平衡是弹出4个字节
//调用者代码:
func3(1, 2, 3);//采用fastcall,前2个参数依次放入ecx和edx寄存器,剩余参数从右往左依次入栈,被调用者负责栈平衡
//0042D743 push 3 //剩余参数从右往左依次入栈,3入栈
//0042D745 mov edx,2 //前2个参数,分别送往ecx和edx寄存器,2入edx
//0042D74A mov ecx,1 //前2个参数,分别送往ecx和edx寄存器,1入ecx
//0042D74F call func3 (42B023h)23h)
x64默认的调用约定是fastcall;fastcall在x64上调用规则:
- 一个函数在调用时,前四个参数是从左至右依次存放于RCX、RDX、R8、R9寄存器里面,剩下的参数从右至左顺序入栈;栈的增长方向为从高地址到低地址。
- 浮点前4个参数传入XMM0、XMM1、XMM2 和 XMM3 中。其他参数传递到堆栈中。
- 调用者负责在栈上分配32字节的“shadow space”,用于存放那四个存放调用参数的寄存器的值(亦即前四个调用参数);小于64位(bit)的参数传递时高位并不填充零(例如只传递ecx),大于64位需要按照地址传递;
- 调用者负责栈平衡;
- 被调用函数的返回值是整数时,则返回值会被存放于RAX;浮点数返回在xmm0中
- RAX,RCX,RDX,R8,R9,R10,R11是“易挥发”的,不用特别保护(所谓保护就是使用前要push备份),其余寄存器需要保护。(x86下只有eax, ecx, edx是易挥发的)
- 栈需要16字节对齐,“call”指令会入栈一个8字节的返回值(注:即函数调用前原来的RIP指令寄存器的值),这样一来,栈就对不齐了(因为RCX、RDX、R8、R9四个寄存器刚好是32个字节,是16字节对齐的,现在多出来了8个字节)。所以,所有非叶子结点调用的函数,都必须调整栈RSP的地址为16n+8,来使栈对齐。比如sub rsp,28h
-
对于 R8~R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。
20.2:传参之汇编
将实参数据传递给函数的方式,分为:
- 传值
- 传指针
- C++中的传引用
传指针和传引用都是针对实参来说的
20.2.1:传值
传值无法改变实参的值(传值时存放在栈上的形参只是实参值的一个拷贝,无法改变实参)。
void func1(int x) {
x=1;
}
int a=0;
func1(a);
010414A5 mov eax,dword ptr [a] //把a的值放入eax中,然后把eax入栈
010414A8 push eax
010414A9 call func1 (10411D6h)
010414AE add esp,4
20.2.2:传指针
传指针:形参x是一个指向整数类型数据的地址。在函数内部通过*运算符来引用实参。
void func2(int *x) {
*x=2;
}
int a=0;
func2(&a);
010414B1 lea eax,[a] //把a的地址传入eax中
010414B4 push eax //把eax入栈
010414B5 call func2 (104100Ah)
010414BA add esp,4
20.2.3:C++中的传引用
C++中的传引用:形参部分使用的是&,而在函数内部,可以直接把形参当做实参来使用,此时形参就是对实参的一个引用。
void func3(int &x) {
x=3;
}
int a=0;
func3(a);
010414BD lea eax,[a] //在汇编层与传指针的方法完全一样
010414C0 push eax
010414C1 call func3 (104102Dh)
010414C6 add esp,4
20.3:C语句之汇编
20.3.1:i++
C语句中的i++对应的汇编语句如下:
int i = 0;
00FC14D4 mov dword ptr [i],0
i++;
00FC14DB mov eax,dword ptr [i]
00FC14DE add eax,1
00FC14E1 mov dword ptr [i],eax
/*
i++在汇编层由多条汇编指令组成。
在多线程环境下,i++不是多线程安全的,
因为它不是原子操作,因为一个线程执行了i++某一条汇编指令,CPU的时间片就有可能用完了,发生了切换,而另外一个线程切换进来之后,又开始从头开始执行i++的汇编指令,因此造成多线程不一致性。
*/
20.3.2:循环语句(for,while,do-while)与汇编
for(int i=0;i<10;i++) {…}
//for循环语句的汇编代码:
汇编代码:
i=0;
jmp A;
B:
i++
A:
cmp i, 0ah//0ah(十六进制) = 10(十进制)
jge OUT;
.....
jmp B;
OUT:
while(i<10) { i++; }
//While循环语句的汇编代码:
A:
cmp i,0ah
jge OUT;
......
i++;
jmp A;
OUT:
后续此章节再补充