从汇编角度分析VS下C++函数调用

记录函数调用的细节,深入汇编层面的。只分析windows平台下,VS C++编译器的实现。

32位系统下

普通函数调用
int Add1(int a, int b)
{
    return a + b;
}
int main()
{
    int sum = Add1(1, 2);
    return 0;
}

生成的汇编代码为:

    int sum = Add1(1, 2);
00041D6A  push        2  
00041D6C  push        1  
00041D6E  call        Add1 (04149Ch)  
00041D73  add         esp,8  
00041D76  mov         dword ptr [sum],eax  

从中观察到的现象是:

  1. 函数调用使用栈传递参数,从右向左入栈;
  2. 函数返回值通过eax寄存器返回(如果返回值的大小不超过32位,4字节);
  3. 调用者负责清理配平栈,将参数占用的空间出栈;
add esp, 8

延伸一个问题:
三种函数调用约定
__cdecl: C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡。
__stdcall: windows API默认方式,参数从右向左入栈,被调函数负责栈平衡。
__fastcall: 快速调用方式。所谓快速,这种方式选择将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左从栈传入。因为栈是位于内存的区域,而寄存器位于CPU内,故存取方式快于内存,故其名曰“__fastcall”。

参考这里:(仅适用于32位系统下)
https://www.cnblogs.com/-qing-/p/10674223.html

这个简单的Add1函数使用的就是__cdecl调用约定。
由Add1的调用者main函数负责栈平衡。

类成员函数调用
大部分情况下使用的__stdcall调用约定,即我们会在函数的最后看到ret 指令后面跟了一个数字,这个即是通过栈传递的参数的尺寸,不包括this指针,this指针通过ecx或rcx传递。
当参数长度可变的时候使用的__cdecl约定,待验证

64位系统下

普通函数调用
int Add(int i, int a, int b, int c= 4, int d=5, int e = 6, int f= 7, int g = 8)
{
    return a + b * c + i;
}

int Add1(int a, int b)
{
    return a + b;
}


int main()
{
    int sum2 = Add(1, 2, 3);
    int sum = Add1(1, 2);

    return 0;
}

main函数的汇编代码

int main()
{
00007FF624101810  push        rbp  
00007FF624101812  push        rdi  
00007FF624101813  sub         rsp,148h  
00007FF62410181A  lea         rbp,[rsp+40h]  
00007FF62410181F  mov         rdi,rsp  
00007FF624101822  mov         ecx,52h  
00007FF624101827  mov         eax,0CCCCCCCCh  
00007FF62410182C  rep stos    dword ptr [rdi]  
00007FF62410182E  lea         rcx,[__AA539275_main@cpp (07FF624111002h)]  
00007FF624101835  call        __CheckForDebuggerJustMyCode (07FF624101082h)  
    int sum2 = Add(1, 2, 3);
00007FF62410183A  mov         dword ptr [rsp+38h],8  
00007FF624101842  mov         dword ptr [rsp+30h],7  
00007FF62410184A  mov         dword ptr [rsp+28h],6  
00007FF624101852  mov         dword ptr [rsp+20h],5  
00007FF62410185A  mov         r9d,4  
00007FF624101860  mov         r8d,3  
00007FF624101866  mov         edx,2  
00007FF62410186B  mov         ecx,1  
00007FF624101870  call        Add (07FF62410101Eh)  
00007FF624101875  mov         dword ptr [sum2],eax  
    int sum = Add1(1, 2);
00007FF624101878  mov         edx,2  
00007FF62410187D  mov         ecx,1  
00007FF624101882  call        Add1 (07FF62410123Fh)  
00007FF624101887  mov         dword ptr [sum],eax  
    return 0;
00007FF62410188A  xor         eax,eax  
}
00007FF62410188C  lea         rsp,[rbp+108h]  
00007FF624101893  pop         rdi  
00007FF624101894  pop         rbp  
00007FF624101895  ret  

详细分析main函数的汇编代码:

rbp入栈      --具体操作伪码rsp -= 8, *rsp = rbp,从栈中申请8字节,rbp的值存入栈顶,将rsp指向栈顶
rdi入栈      --同上
从栈中分配148h字节,16*16 + 4*16+8=328字节
rbp = rsp + 40h     --这里的40h = 64字节,即为Add调用使用的栈空间
将申请的栈空间全部初始化为0CCCCCCCCh  --中断指令
忽略如下两条指令,vs添加的调试指令
00007FF62410182E  lea         rcx,[__AA539275_main@cpp (07FF624111002h)]  
00007FF624101835  call        __CheckForDebuggerJustMyCode (07FF624101082h)  
--Add函数调用
将四个参数放入申请的栈空间中; --注意这里都是使用的8字节对齐,同时注意具体的偏移,参数存储的位置是申请栈空间的后32个字节 = 4个8字节
前4个参数使用寄存器传递;
调用Add函数;  --返回值在eax中
将返回值拷贝到sum2中
--Add1函数调用
2存入edx
1存入ecx
调用Add1
返回值存入sum
eax=0
清理申请的栈空间,lea         rsp,[rbp+108h]  --之前rbp指向的位置为rsp + 40h, 所以这里总共清理的栈空间为148h字节,跟代码第3行,申请的空间大小是一样的。
rdi出栈
rbp出栈  --申请的所有栈空间全部还给了系统
函数返回

这里我定义了两个函数
Add: 有8个参数,一个返回值
Add1:有2个参数,一个返回值

看到的现象是:
调用Add时,参数从右向左入栈,后4个参数使用栈传递,前4个参数使用寄存器传递;
Add1参数都是用寄存器传递,返回值都用eax返回,因为返回值是4字节,如果是8字节,就是使用rax寄存器。
(这也是64位系统的进步,有更多更大的寄存器可供使用,函数调用优先使用寄存器传递,效率更高,此处仅代表参数传递效率更高,不代表64位程序就一定比32位快,还有其他因素,比如地址长度,指令长度问题等。)
在Add函数的汇编代码中没有看到ret指令后添加数字,说明是被调用者调整栈。

我在Add函数掉用的后面,紧接着Add1函数调用。本来是期望看到

add rsp, 0x40h

由调用者清理栈。
结果看到的是Add1的函数调用参数传递:将参数拷贝到调用者寄存器
即在此处没有栈清理操作。

下面是Add函数的汇编代码:

int Add(int i, int a, int b, int c= 4, int d=5, int e = 6, int f= 7, int g = 8)
{
00007FF7E08F1AB0  mov         dword ptr [rsp+20h],r9d  
00007FF7E08F1AB5  mov         dword ptr [rsp+18h],r8d  
00007FF7E08F1ABA  mov         dword ptr [rsp+10h],edx  
00007FF7E08F1ABE  mov         dword ptr [rsp+8],ecx  
00007FF7E08F1AC2  push        rbp  
00007FF7E08F1AC3  push        rdi  
00007FF7E08F1AC4  sub         rsp,0E8h  
00007FF7E08F1ACB  lea         rbp,[rsp+20h]  
00007FF7E08F1AD0  mov         rdi,rsp  
00007FF7E08F1AD3  mov         ecx,3Ah  
00007FF7E08F1AD8  mov         eax,0CCCCCCCCh  
00007FF7E08F1ADD  rep stos    dword ptr [rdi]  
00007FF7E08F1ADF  mov         ecx,dword ptr [rsp+108h]  
00007FF7E08F1AE6  lea         rcx,[__AA539275_main@cpp (07FF7E0902028h)]  
00007FF7E08F1AED  call        __CheckForDebuggerJustMyCode (07FF7E08F10A0h)  
    return a + b * c + i;
00007FF7E08F1AF2  mov         eax,dword ptr [b]  
00007FF7E08F1AF8  imul        eax,dword ptr [c]  
00007FF7E08F1AFF  mov         ecx,dword ptr [a]  
00007FF7E08F1B05  add         ecx,eax  
00007FF7E08F1B07  mov         eax,ecx  
00007FF7E08F1B09  add         eax,dword ptr [i]  
}
00007FF7E08F1B0F  lea         rsp,[rbp+0C8h]  
00007FF7E08F1B16  pop         rdi  
00007FF7E08F1B17  pop         rbp  
00007FF7E08F1B18  ret  

有几个地方值得分析一下:

  1. 在函数的入口地方,从寄存器中提取前4个参数,存入栈中
  2. 第一个参数存的地方是rsp + 8, 也就是栈顶往上8个字节的地方。因为在调用call指令的时候,会自动把call指令的下一条指令的地址入栈。ret指令会从栈顶出栈8个字节,存入指令寄存器(rpi),程序从之前call指令的下一条指令开始继续执行
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容