阅读本章前,我们先思考两个问题。
函数执行时,程序流程会转到函数体的实现地址,只有遇到return语句或者“}”符号才返回到下一条语句的地址,那么编译器是如何确定应该回到什么地址的呢?为什么很多高级语言在传递参数时会执行将实参复制给形参这一操作呢?
思考5分钟后,请仔细阅读本章,验证你的想法。
6.1 栈帧的形成和关闭
栈在内存中是一块特殊的存储空间,它的存储原则是“先进后出”,即最先被存储的数据最后被释放。汇编过程通常使用PUSH指令与POP指令对栈空间执行数据压入和数据弹出的操作。栈的结构示意图如图6-1所示。

图6-1 栈的结构示意图
图6-1为栈结构在内存中的表现形式。栈结构在内存中占用一段连续的存储空间,通过esp和ebp这两个栈指针寄存器保存当前栈的起始地址与结束地址(又称为栈顶与栈底)。在32位程序的栈结构中,每4字节的栈空间保存一个数据;在64位程序的栈结构中,每8字节的栈空间保存一个数据。像这样的栈顶到栈底之间的存储空间被称为栈帧。
栈帧是如何形成的呢?当栈顶指针esp小于栈底指针ebp时,就形成了栈帧。通常,在C++中,栈帧可以寻址局部变量、函数返回地址、函数参数等数据。
不同的两次函数调用,形成的栈帧也不相同。当一个函数调用另一个函数时,就会针对调用的函数开辟所需的栈空间,形成此函数的栈帧。当这个函数结束调用时,需要清除它使用的栈空间,关闭栈帧,我们把这一过程称为栈平衡。
为什么要进行栈平衡呢?这就像借钱一样,“有借有还,再借不难”。如果某一函数开辟了新的栈空间后没有进行恢复,或者过度恢复,就会造成栈空间上溢或下溢,极有可能给程序带来致命错误,如图6-2所示。

图6-2 栈平衡错误
图6-2中的错误是由栈平衡引发的,只有在Debug版才会出现这个错误提示,方便开发人员找到错误并修正。
6.2 各种调用方式的考察
6.1节介绍了栈结构的相关知识。进入函数时会打开栈空间,退出函数时会还原栈空间。在C++中,通常使用栈传递函数参数,因此传递函数的栈也属于被调用函数栈空间的一部分。汇编过程中通常使用“ret xxxx”平衡参数使用的栈空间,当函数的参数为不定参数时,函数自身无法确定参数使用的栈空间的大小,因此无法由函数自身执行平衡操作,需要函数的调用者执行平衡操作。为了确定参数的平衡者以及参数的传递方式,我们定义了函数的调用约定。C++环境下的调用约定有3种:_cdecl、_stdcall、_fastcall。这3种调用约定的解释如下。
_cdecl:C\C++默认的调用方式,调用方平衡栈,不定参数的函数可以使用这种方式。
_stdcall:被调方平衡栈,不定参数的函数无法使用这种方式。
_fastcall:寄存器方式传参,被调方平衡栈,不定参数的函数无法使用这种方式。
当函数参数个数为0时,无须区分调用方式,使用_cdecl和_stdcall都一样。而大部分函数都是有参数的,通过查看平衡栈即可还原对应的调用方式。我们通过代码清单6-1分析_cdecl与_stdcall这两种调用方式的区别。
代码清单6-1 _cdecl与_stdcall的对比(Debug版)
// C++源码
#include <stdio.h>
void _stdcall show1(int n){ //使用_stdcall调用方式,被调方平衡栈
printf("%d\n", n);
}
void _cdecl show2(int n){ //使用_cdecl调用方式,调用方平衡栈
printf("%d\n", n);
}
int main(int argc, char* argv[]) {
show1(5); // 不会有平衡栈操作
show2(5); // 函数调用结束后,对esp平衡4个字节
return 0;
}
//x86_vs对应汇编代码讲解
00401040 push ebp ;保存栈底
00401041 mov ebp, esp ;保存调用方的栈底
00401043 push 5 ;函数传参,参数1入栈
00401045 call sub_401000 ;调用show1函数,没有平衡操作
0040104A push 5 ;函数传参,参数1入栈
0040104C call sub_401020 ;调用show2函数
00401051 add esp, 4 ;平衡栈顶
00401054 xor eax, eax
00401056 pop ebp ;恢复环境
00401057 retn
00401000 push ebp ;保存栈底
00401001 mov ebp, esp ;保存调用方的栈底
00401003 mov eax, [ebp+8]
00401006 push eax
00401007 push offset aD ; "%d\n"
0040100C call sub_4010A0
00401011 add esp, 8
00401014 pop ebp ;恢复环境
00401015 retn 4 ;show1函数结束后平衡栈顶4个字节,等价esp+=4
00401020 push ebp ;保存栈底
00401021 mov ebp, esp ;保存调用方的栈底
00401023 mov eax, [ebp+8]
00401026 push eax
00401027 push offset aD_0 ; "%d\n"
0040102C call sub_4010A0
00401031 add esp, 8
00401034 pop ebp ;恢复环境
00401035 retn ;show2函数结束后没有平衡操作
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解(Debug版)
00411941 push 5 ; 函数传参,参数1入栈
00411943 call j_show1 ; 调用show1函数,没有平衡操作
00411948 push 5 ; 函数传参,参数1入栈
0041194A call j_show2 ; 调用show2函数
0041194F add esp, 4 ; 平衡栈顶
00411952 xor eax, eax
00411954 pop edi
00411955 pop esi
00411956 pop ebx
00411957 add esp, 0C0h
0041195D cmp ebp, esp
0041195F call j__RTC_CheckEsp
00411964 mov esp, ebp
00411966 pop ebp ; 恢复环境
00411967 retn
......
00411780 push ebp ; 保存栈底
00411781 mov ebp, esp ; 保存调用方的栈底
00411783 sub esp, 0C0h
00411789 push ebx
0041178A push esi
0041178B push edi
0041178C mov edi, ebp
0041178E xor ecx, ecx
00411790 mov eax, 0CCCCCCCCh
00411795 rep stosd
00411797 mov ecx, offset unk_41C003
0041179C call j___CheckForDebuggerJustMyCode
004117A1 mov eax, [ebp+arg_0]
004117A4 push eax
004117A5 push offset aD ; "%d\n"
004117AA call j_printf
004117AF add esp, 8
004117B2 pop edi
004117B3 pop esi
004117B4 pop ebx
004117B5 add esp, 0C0h
004117BB cmp ebp, esp
004117BD call j__RTC_CheckEsp
004117C2 mov esp, ebp
004117C4 pop ebp ; 恢复环境
004117C5 retn 4 ; show1函数结束后平衡栈顶4个字节,等价esp+=4
.......
004117E0 push ebp ; 保存栈底
004117E1 mov ebp, esp ; 保存调用方的栈底
004117E3 sub esp, 0C0h
004117E9 push ebx
004117EA push esi
004117EB push edi
004117EC mov edi, ebp
004117EE xor ecx, ecx
004117F0 mov eax, 0CCCCCCCCh
004117F5 rep stosd
004117F7 mov ecx, offset unk_41C003
004117FC call j___CheckForDebuggerJustMyCode
00411801 mov eax, [ebp+arg_0]
00411804 push eax
00411805 push offset aD ; "%d\n"
0041180A call j_printf
0041180F add esp, 8
00411812 pop edi
00411813 pop esi
00411814 pop ebx
00411815 add esp, 0C0h
0041181B cmp ebp, esp
0041181D call j__RTC_CheckEsp
00411822 mov esp, ebp
00411824 pop ebp ; 恢复环境
00411825 retn ; show2函数结束后没有平衡操作



笔记--------------------------------------------end
分析代码清单6-1可知,_cdecl调用方式在函数内没有任何平衡参数操作,而在退出函数后对esp执行了加4操作,从而实现栈平衡,_stdcall调用方式则与之相反。那么,是不是只要检查ret处是否有平衡操作即可得知函数的调用方式呢?由于汇编语言灵活多变,这种方法无法保证分析结果的准确性。在函数的结尾处,很有可能有其他汇编指令间接对esp做加法,如pop ecx这样的指令也可达到栈平衡的效果,而且指令周期较短。因此,还需要结合函数在执行过程中使用的栈空间与函数调用结束时的栈平衡数进行对比,以判断是否实现参数平衡。
C语言中经常使用的printf函数用的就是典型的_cdecl调用方式,因为printf的参数可以有多个,所以只能以_cdecl方式调用。那么,当printf函数被多次使用后,会在每次调用结束后进行栈平衡操作吗?在Debug版下,为了匹配源码会这样做。而经过02选项的优化后,会采取复写传播优化,将每次参数平衡的操作进行归并,一次性平衡栈顶指针esp,如代码清单6-2所示。
代码清单6-2 _cdecl参数平衡代码的复写传播优化(Release版)
// C++ 源码说明:复写传播
void main(){
printf("Hello ");// 函数调用结束后,执行eps+4平衡参数
printf("World"); // 同上
printf(" C++"); // 同上
printf("\r\n"); // 同上,经过优化后,将4次平衡归并为1次
}
//x86_vs对应汇编代码讲解
00401010 push offset aHello ; "Hello"
00401015 call sub_401040 ;调用结束后没有平衡栈
0040101A push offset aWorld ; "World"
0040101F call sub_401040 ;调用结束后没有平衡栈
00401024 push offset aC ; "C++"
00401029 call sub_401040 ;调用结束后没有平衡栈
0040102E push offset asc_412178 ; "\r\n"
00401033 call sub_401040 ;调用结束后没有平衡栈
00401038 add esp, 10h ;一次性对esp加16,正好平衡了之前的4个参数
0040103B xor eax, eax
0040103D retn
//x86_gcc对应汇编代码讲解
00402590 push ebp ;保存栈底
00402591 mov ebp, esp ;保存调用方的栈底
00402593 and esp, 0FFFFFFF0h ;对齐栈
00402596 sub esp, 10h
00402599 call ___main ;调用初始化函数
0040259E mov dword ptr [esp], offset aHello ; "Hello "
004025A5 call _printf ;调用结束后没有平衡栈
004025AA mov dword ptr [esp], offset aWorld ; "World"
004025B1 call _printf ;调用结束后没有平衡栈
004025B6 mov dword ptr [esp], offset aC ; " C++"
004025BD call _printf ;调用结束后没有平衡栈
004025C2 mov dword ptr [esp], offset asc_404012 ; "\r"
004025C9 call _puts ;调用结束后没有平衡栈
004025CE xor eax, eax
004025D0 leave ;mov esp,ebp,正好平衡了之前的4个参数和局部变量空间
004025D1 retn
//x86_clang对应汇编代码没有优化,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Release版)
00401040 push offset aHello ; "Hello "
00401045 call printf ; 调用结束后没有平衡栈
0040104A push offset aWorld ; "World"
0040104F call printf ; 调用结束后没有平衡栈
00401054 push offset aC ; " C++"
00401059 call printf ; 调用结束后没有平衡栈
0040105E push offset asc_402118 ; "\r\n"
00401063 call printf ; 调用结束后没有平衡栈
00401068 add esp, 10h ; 一次性对esp加16,正好平衡了之前的4个参数
0040106B xor eax, eax
0040106D retn

笔记--------------------------------------------end
通过以上分析发现,_cdecl与_stdcall只在参数平衡上有所不同,其余部分都一样。但经过优化后,_cdecl调用方式的函数在同一作用域内多次使用,比_stdcall的效率高一些,这是因为_cdecl可以使用复写传播,而_stdcall都在函数内平衡参数,无法使用复写传播这种优化方式。在这3种调用方式中,_fastcall调用方式的传参效率最高,其他两种调用方式都是通过栈传递参数的,唯独_fastcall可以利用寄存器传递参数。因为寄存器数目很少,而参数可以很多,只能量力而行,所以_fastcall调用方式只使用了ecx和edx,分别传递第一个参数和第二个参数,其余参数则通过栈传参方式传递,如代码清单6-3所示。
代码清单6-3 _fastcall调用方式示例
// C++源码
#include <stdio.h>
void _fastcall show(int n1, int n2, int n3, int n4) {
printf("%d %d %d %d\n", n1, n2, n3, n4);
}
int main(int argc, char* argv[]) {
show(1, 2, 3, 4);
return 0;
}
//x86_vs对应汇编代码讲解
00401030 push ebp ;保存栈底
00401031 mov ebp, esp ;保存调用方的栈底
00401033 push 4 ;使用栈方式传递参数4
00401035 push 3 ;使用栈方式传递参数3
00401037 mov edx, 2 ;使用edx传递第二个参数2
0040103C mov ecx, 1 ;使用ecx传递第一个参数1
00401041 call sub_401000 ;调用show函数
00401046 xor eax, eax
00401048 pop ebp ;恢复环境
00401049 retn
00401000 push ebp ;保存栈底
00401001 mov ebp, esp ;保存调用方的栈底
00401003 sub esp, 8
00401006 mov [ebp-4], edx ;保存n2到ebp-4
00401009 mov [ebp-8], ecx ;保存n1到ebp-8
0040100C mov eax, [ebp+0Ch]
0040100F push eax ;参数5入栈,n4
00401010 mov ecx, [ebp+8]
00401013 push ecx ;参数4入栈,n3
00401014 mov edx, [ebp-4]
00401017 push edx ;参数3入栈,n2
00401018 mov eax, [ebp-8]
0040101B push eax ;参数2入栈,n1
0040101C push offset aDDDD ;参数1入栈,"%d %d %d %d\n"
00401021 call sub_401090 ;调用printf函数
00401026 add esp, 14h ;平衡pirntf使用的5个参数
00401029 mov esp, ebp
0040102B pop ebp ;恢复环境
0040102C retn 8 ;此函数有4个参数,ret指令对其平衡
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Debug版)
004118F1 push 4 ; 使用栈方式传递参数4
004118F3 push 3 ; 使用栈方式传递参数3
004118F5 mov edx, 2 ; 使用edx传递第二个参数2
004118FA mov ecx, 1 ; 使用ecx传递第一个参数1
004118FF call j_show ; 调用show函数
00411904 xor eax, eax
00411906 pop edi
00411907 pop esi
00411908 pop ebx
00411909 add esp, 0C0h
0041190F cmp ebp, esp
00411911 call j__RTC_CheckEsp
00411916 mov esp, ebp
00411918 pop ebp ; 恢复环境
00411919 retn
......
0041179D mov eax, [ebp+arg_4] ; 参数5入栈,n4
004117A0 push eax
004117A1 mov ecx, [ebp+arg_0] ; 参数4入栈,n3
004117A4 push ecx
004117A5 mov edx, [ebp+var_14] ; 参数3入栈,n2
004117A8 push edx
004117A9 mov eax, [ebp+var_8] ; 参数2入栈,n1
004117AC push eax
004117AD push offset aDDDD ; 参数1入栈 "%d %d %d %d\n"
004117B2 call j_printf ; 调用printf函数
004117B7 add esp, 14h ; 平衡pirntf使用的5个参数
004117BA pop edi
004117BB pop esi
004117BC pop ebx
004117BD add esp, 0D8h
004117C3 cmp ebp, esp
004117C5 call j__RTC_CheckEsp
004117CA mov esp, ebp
004117CC pop ebp ; 恢复环境
004117CD retn 8 ; 此函数有4个参数,ret指令对其平衡


//x86_vs对应汇编代码讲解 (Release版)
00401040 push 4 ; 参数5入栈
00401042 push 3 ; 参数4入栈
00401044 push 2 ; 参数3入栈
00401046 push 1 ; 参数2入栈
00401048 push offset aDDDD ; "%d %d %d %d\n"
0040104D call printf ; 调用printf函数
00401052 add esp, 14h ; 平衡pirntf使用的5个参数
00401055 xor eax, eax
00401057 retn

Release优化后,把函数的汇编直接嵌入main函数里了,省掉了单独的函数调用。
笔记--------------------------------------------end
6.3 使用ebp或esp寻址
在前面的内容中,我们接触到了很多高级语言中的变量访问。将高级语言转换成汇编代码后,就变成了对ebp或esp进行加减法操作(寄存器相对间接寻址方式)获取变量在内存中的数据,代码如下所示。
// 变量n所在地址为ebp-4,对这个变量进行访问其实就是按dword方式读写这个地址
int n = 1;
0040B7C8 mov dword ptr [ebp-4],1
// 变量ch所在地址为ebp-8,对这个变量进行访问其实就是按byte方式读写这个地址
char ch = 2;
0040B7CF mov byte ptr [ebp-8],2
由此可见,局部变量是通过栈空间保存的。上述两个变量采用了ebp寻址方式,这说明在内存中,局部变量是以连续排列的方式存储在栈内的。
由于局部变量使用栈空间进行存储,因此进入函数后的第一件事就是开辟函数中局部变量所需的栈空间。这时函数中的局部变量就有了各自的内存空间,在函数结尾处执行释放栈空间的操作。因此局部变量是有生命周期的,它的生命周期在进入函数体的时候开始,在函数执行结束的时候终止。
在大多数情况下,使用ebp寻址局部变量只能在非02选项中产生,这样做是为了方便调试和检测栈平衡,使目标代码可读性更高。从代码清单6-1可以看出,使用ebp保存函数作用域的栈地址,在函数退出前用于esp的还原以及栈平衡的检查。而在02编译选项中,为了提升程序的效率,省去了这些检测工作,在用户编写的代码中,只要栈顶是稳定的,就可以不再使用ebp,利用esp直接访问局部变量,因此节省一个寄存器资源。为了防止变量被编译器优化掉,需要对变量做一些输入输出操作,如代码清单6-4所示。
代码清单6-4 使用esp访问局部变量(Release版)
// C++源码
#include <stdio.h>
int main(int argc, char* argv[]) {
int n = 1;
scanf("%d", &n);
char ch = 2;
scanf("%c", &ch);
printf("%d %c\n", n, ch);
return 0;
}
//x86_vs对应汇编代码讲解
00401020 var_5 = byte ptr -5 ;IDA定义的局部变量标号,IDA环境下局部变量用var_开头
00401020 var_4 = dword ptr -4 ;IDA定义的局部变量标号
00401020
00401020 sub esp, 8 ;局部变量栈空间开辟,开辟8字节栈空间
00401023 lea eax, [esp+8+var_4] ;这句指令等价于:esp+8-4,标号var_4等于-4,IDA自动
;识别出来访问的变量地址,并调整显示方式,
;省去了我们自己计算偏移量这个过程,
;类似于高级语言中为变量起名字,使得显示更具可读性
00401027 mov [esp+8+var_4], 1 ;初始化var_4变量为1
0040102F push eax ;eax中保存[esp+8-4]的值,将eax作为参数入栈
00401030 push offset unk_417160 ;"%d"
00401035 call sub_4010A0 ;调用scanf函数
0040103A lea eax, [esp+10h+var_5];在分析指令的时候,IDA会根据代码上下文归纳出影响栈顶
;的指令,以确定esp相对寻址所访问的目标。于是IDA识
;别出以下相对寻址指令的目标是该函数中的局部变量var_5,因为在var_5前执行了2次push指令,所以esp
;指向的栈顶地址存在-8的差值,而且本函数第一条指令sub
;esp, 8也是影响栈顶的。综合以上信息,IDA为了表达
;出此时访问的局部变量为var_5,而var_5又被定义为-5,
;则需要对esp相对寻址做调整了,先求解[esp+X+var_5]
;中的X,此处求解的X值为10h,然后就可以表达为
;[esp+10h+var_5],以加强代码的可读性。
;笔者建议读者对此问题可以在调试器环境下观察栈窗口,
;自己计算一下,加深其后讲解的体会和理解
0040103E mov [esp+10h+var_5], 2 ;为var_5处的局部变量赋值2
00401043 push eax ;功能同上,esp -= 4
00401044 push offset unk_417164 ;"%c",esp-=4
00401049 call sub_4010A0 ;功能同上
0040104E movsx eax, [esp+18h+var_5];因为又执行了两次push指令,并且没有平衡栈,所以需要
;再次调整esp的相对偏移值,这里的调整值为18h。注
;意,在这里的movsx指令处点一下Q键,可以得到movsx
;edx, byte ptr [esp+13h],按K键可还原名称。
;这里的movsx指令暴露出var_5的类型为有符号类型,
;byte ptr说明长度为单字节,对应C语言中的定义就应
;该是char了。当然读者也可以考察使用变量作参数的
;函数,如果函数功能是已知的,那么参数类型也已知
;了,进而可推导变量的类型,如果遇到本例这类格式化
;函数,那么鉴定变量类型就更简单了
00401053 push eax ;esp -= 4
00401054 push [esp+1Ch+var_4] ;esp -= 4
00401058 push offset aDC ;"%d %c\n", esp -= 4
0040105D call sub_401070 ;调用printf函数
00401062 xor eax, eax
00401064 add esp, 24h ;经过优化后代码,一次性平衡了栈顶esp。在此函数中,
;共执行了7次push操作,而函数scanf和printf函数
;都是__cdecl调用方式,所以函数内没有平衡栈,需要
;调用者来平衡栈顶指针esp,又因为在退出函数前,还
;需释放局部变量的8字节(见函数入口指令)空间,因
;此esp需要加7*4+8=36转换成十六进制后的24h执行
00401067 retn ;ret指令结束函数调用
//x86_gcc对应汇编代码讲解
00402590 argc = dword ptr 8 ;IDA定义的局部变量标号
00402590 argv = dword ptr 0Ch ;IDA定义的局部变量标号
00402590 envp = dword ptr 10h ;IDA定义的局部变量标号
00402590
00402590 push ebp ;保存栈底
00402591 mov ebp, esp ;保存调用方的栈底
00402593 and esp, 0FFFFFFF0h ;esp低4位置0,保证栈模4对齐
00402596 sub esp, 20h ;申请32字节变量空间
00402599 call ___main ;调用初始化函数
0040259E lea eax, [esp+1Ch] ;eax中保存[esp+1C]的地址
004025A2 mov dword ptr [esp], offset aD ;"%d",参数入栈
004025A9 mov [esp+4], eax ;eax中保存[esp+1C]的值,将eax作为参数入栈
004025AD mov dword ptr [esp+1Ch], 1;初始化[esp+1Ch]变量为1
004025B5 call _scanf ;调用scanf函数
004025BA lea eax, [esp+1Bh] ;eax中保存[esp+1Bh]的地址
004025BE mov byte ptr [esp+1Bh], 2 ;初始化[esp+1Bh]变量为2
004025C3 mov [esp+4], eax ;eax中保存[esp+1Bh]的值,将eax作为参数入栈
004025C7 mov dword ptr [esp], offset aC ;"%c",参数入栈
004025CE call _scanf ;调用scanf函数
004025D3 movsx eax, byte ptr [esp+1Bh] ;eax保存[esp+1Bh]的值
004025D8 mov dword ptr [esp], offset aDC ;"%d %c\n",参数入栈
004025DF mov [esp+8], eax ;将eax作为参数入栈
004025E3 mov eax, [esp+1Ch] ;eax保存[esp+1Ch]的值
004025E7 mov [esp+4], eax ;将eax作为参数入栈
004025EB call _printf ;调用printf函数
004025F0 xor eax, eax
004025F2 leave ;等价于mov esp, ebp,pop ebp
;一次性平衡了栈顶esp
004025F3 retn ;执行ret指令结束函数调用
//x86_clang对应汇编代码讲解
00401000 var_5 = byte ptr -5 ;IDA定义的局部变量标号
00401000 var_4 = dword ptr -4 ;IDA定义的局部变量标号
00401000
00401000 sub esp, 8 ;申请8字节变量空间
00401003 lea eax, [esp+8+var_4] ;eax中var_4变量的地址
00401007 mov [esp+8+var_4], 1 ;初始化var_4变量为1
0040100F push eax ;eax中保存var_4的值,将eax作为参数入栈
00401010 push offset unk_417160 ;"%d",参数入栈
00401015 call sub_401060 ;调用scanf函数
0040101A add esp, 8 ;平衡栈
0040101D lea eax, [esp+8+var_5] ;eax中var_5变量的地址
00401021 mov [esp+8+var_5], 2 ;初始化var_5变量为2
00401026 push eax ;eax中保存var_5的值,将eax作为参数入栈
00401027 push offset unk_417163 ;"%c",参数入栈
0040102C call sub_401060 ;调用scanf函数
00401031 add esp, 8 ;平衡栈
00401034 movsx eax, [esp+8+var_5] ;eax中保存var_5的值
00401039 push eax ;将eax作为参数入栈
0040103A push [esp+0Ch+var_4] ;将var_4的值作为参数入栈
0040103E push offset aDC ;"%d %c\n",参数入栈
00401043 call sub_4010A0 ;调用printf函数
00401048 add esp, 0Ch ;平衡栈
0040104B xor eax, eax
0040104D add esp, 8 ;平衡局部变量空间的栈
00401050 retn ;执行ret指令结束函数调用
笔记--------------------------------------------start

这是编译器添加的栈溢出检测机制:
- 函数开头:将一个安全cookie(随机值)与
ebp异或后保存在栈上 - 函数结尾:取出cookie,再次与
ebp异或,检查是否被修改 - 如果cookie变了,说明发生了栈溢出,程序会终止
栈布局图示:

源代码的2个变量对应 var_C 和 var_5
第3个 var_4 是编译器自动添加的安全cookie,用于栈保护
可以通过编译选项 -GS-(Visual Studio)或 -fno-stack-protector(GCC)来禁用这个保护,那时就只会看到2个局部变量了

重新编译:
//x86_vs对应汇编代码讲解 (Release版)
00401080 push ebp
00401081 mov ebp, esp
00401083 sub esp, 8 ; 局部变量栈空间开辟,开辟8字节栈空间
00401086 lea eax, [ebp+var_8] ; 这句指令等价于:esp-8,标号var_8等于-8,
00401086 ; IDA自动识别出来访问的变量地址,并调整显示
00401086 ; 方式,省去了我们自己计算偏移量这个过程,类似
00401086 ; 于高级语言中为变量起名字,使得显示更具可读性
00401089 mov [ebp+var_8], 1 ; 初始化变量var_8 = 1
00401090 push eax ; eax中保存[esp-8]的值,将eax作为参数入栈
00401091 push offset aD ; "%d"
00401096 call scanf ; 调用scanf函数
0040109B lea eax, [ebp+var_1] ; 在分析指令的时候,IDA会根据代码上下文归纳出影响栈顶的
0040109B ; 指令,以确定esp相对寻址所访问的目标。于是IDA识别出以下
0040109B ; 相对寻址指令的目标是该函数中的局部变量var_1
0040109E mov [ebp+var_1], 2 ; 初始化变量var_1=2
004010A2 push eax ; eax中保存[esp-1]的值,将eax作为参数入栈
004010A3 push offset aC ; "%c"
004010A8 call scanf ; 调用scanf函数
004010AD movsx eax, [ebp+var_1] ; 按Q键得到movsx eax, byte ptr [ebp-1],
004010AD ; 按K键还原名称var_1赋给al,并扩展符号位到eax
004010B1 push eax ; var_1入栈
004010B2 push [ebp+var_8] ; var_8入栈
004010B5 push offset aDC ; "%d %c\n"
004010BA call printf ; 调用printf函数
004010BF add esp, 1Ch ; 前面有7个push操作,这里恢复栈平衡
004010BF ; 7*4=28 = 1Ch
004010C2 xor eax, eax ; eax=0
004010C4 mov esp, ebp ; 恢复esp的初始值
004010C6 pop ebp ; 恢复ebp的初始值
004010C7 retn ; ret指令结束函数调用

笔记--------------------------------------------end
在代码清单6-4中,通过IDA的标识,可以轻松获得函数实现中的两个局部变量。图6-3为变量在栈中占用的地址空间,假设进入函数后未分配栈空间的esp为0x0012FFF0。

图6-3 使用esp寻址栈空间
使用esp寻址后,不必在每次进入函数后都调整栈底ebp,这样既减少了ebp的使用,又省去了维护ebp的相关指令,因此可以有效提升程序的执行效率。但是,缺少了ebp就无法保存进入函数后的栈底指针,也就无法进行栈平衡检测。因为程序已经是Release版,在程序发布前经过Debug下的调试检测,所以这项检测工作有些画蛇添足,可以省去。
每次访问变量都需要计算,如果在函数执行过程中esp发生了改变,再次访问变量就需要重新计算偏移,这真是个令人头疼的问题。为了省去对偏移量的计算,方便分析,IDA在分析过程中事先将函数中每个变量的偏移值计算出来,得出一个固定偏移值,用标号将其记录。那么IDA是如何计算这个固定偏移值的呢?
在进入函数后,执行申请变量栈空间的相关指令,调整esp,然后以调整前的esp作为基址计算局部变量的偏移值,即图6-3中的地址0x0012FFF0。在函数的执行过程中,如果存在对栈顶操作的相关指令,则调整esp相对间接寻址中的相对值,再加上标号值,寻址到变量所在的地址。被调整的esp的相对值是负数,这样一来,因为使用的是调整前的esp作为基址,而栈顶的生长方向是向0增长,所以变量的偏移值必然为负数。
假设esp进入函数前地址为0x0012FFF0,示例中使用如下两个整型变量。
var_0 = -4 ; 定义第一个变量偏移量,所在地址为0x0012FFEC
var_1 = -8 ; 定义第二个变量偏移量,所在地址为0x0012FFE8
sub esp, 8 ; 申请变量栈空间,esp保存地址变为0x0012FFE8
;使用申请变量栈空间前的esp作为基址,就需要调整 esp,将其加8
lea eax, [esp+8+var_0]
push eax ; 执行push指令,esp被减4,esp地址变为0x0012FFE4
;因为esp被减4,所以需要对基址esp进行二次调整,加8后再加4,
;最终得到数值0x0C
lea eax [esp+0Ch+var_1]
6.4 函数的参数
6.2节分析的示例函数没有参数,如果在函数的调用过程中有使用栈顶传参的情况,那么esp会有哪些变化呢?
假设当前esp为0x0012FF10,传递参数如下。
push 5 ; 指令执行后esp-4等于0x0012FF0C
push 6 ; 指令执行后esp-4等于0x0012FF08
; 函数调用,call指令会将下一条指令地址压栈,作为函数的返回地址,本节暂不讨论
call xxxx
函数参数通过栈结构进行传递,在C++代码中,其传参顺序为从右向左依次入栈,最先定义的参数最后入栈。参数也是函数中的一个变量,采用正数标号法表示局部变量偏移标号时,函数的参数标号和局部变量的标号值都为正,无法区分,不利于分析。使用负数标号法表示就可以将两者区分了:正数表示参数,负数表示局部变量,0表示返回地址(对返回地址的讲解见6.5节)。这样,用户在对反汇编代码进行分析时就省去了计算偏移量的工作,只须查看标号名称就可得知在访问哪一个变量。根据寻址过程中的计算方式可知访问的变量是局部变量还是参数。
Debug版下的分析相对简单,由于其注重调试功能,因此使用的是ebp寻址方式。在进入函数时,已将ebp调整至当前作用域的栈底,可直接使用。
因为函数的传参是通过栈方式传递的,使用push指令将数据压入栈中,而push指令将操作数复制到栈顶,所以这时压入栈中的数据和原数据是独立存在的,在两个不同地址处。因此对函数参数的修改,实际上是对当前函数栈内的参数中保存的值进行修改,与原数据没有任何关系。正因如此,在C\C++中,形参是实参的副本,对形参的修改不影响实参,示例如代码清单6-5所示。
代码清单6-5 函数参数传递(Release版)
// C++源码
#include <stdio.h>
void addNumber(int n1){
n1 += 1;
printf("%d\n", n1);
}
int main(int argc, char* argv[]) {
int n = 0;
scanf("%d", &n); // 防止变量被常量扩散优化
addNumber(n);
return 0;
}
//x86_vs对应汇编代码讲解
004010A0 var_4 = dword ptr -4 ;局部变量标号定义,main()函数中只有一个局部变量
004010A0
004010A0 push ecx ;注意这里的push ecx,请读者现在定位到函数末尾去看
;看,是不是发现没有pop ecx?所以这里并不是保存寄存
;器环境,而是使用低周期的push ecx代替高周期的sub
;esp,4,强度削弱。这样就有了局部变量的空间
004010A1 lea eax,[esp+4+var_4] ;取出局部变量地址存入eax
004010A4 mov [esp+4+var_4], 0 ;将局部变量赋值为0
004010AB push eax ;压入eax作为参数,eax中保存局部变量地址
004010AC push offset unk_417164 ;"%d"
004010B1 call sub_4010F0 ;调用scanf函数
004010B6 push [esp+0Ch+var_4] ;局部变量的值入栈,结合函数sub_401000分析此处是否为
;参数压栈,考察函数内有没有对其引用,有没有使用ret
;指令平衡参数
004010BA call sub_401000 ;调用函数,标号为sub_401000,双击可跟进到函数实现中
004010BF xor eax, eax
004010C1 add esp, 10h ;退出前平衡栈顶esp,共使用4次push指令,由此得出函数
;sub_401000为__cdecl调用方式,使用了1个参数
004010C4 retn
00401000 arg_0 = dword ptr 4 ;正数,为参数标号。在IDA下参数以arg_为前缀
00401000 mov eax, [esp+arg_0] ;访问第一个参数,取出数据到eax中,此函数就一个参数
00401004 inc eax ;对参数内容加1,main()函数中压入的参数为5
00401005 push eax ;将加1后的eax压入栈中,作为printf函数参数
00401006 push offset unk_417160 ;"%d\n"
0040100B call sub_4010D0 ;调用printf函数
00401010 add esp, 8 ;平衡printf函数使用的两个参数
00401013 retn ;函数内没有平衡参数,可以见此函数为__cdecl调用方式
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Release版)
00401080 push ebp
00401081 mov ebp, esp ; 恢复栈平衡 对上面的push ebp而言
00401083 push ecx
00401084 lea eax, [ebp+var_4] ; eax关联变量var_4的地址
00401087 mov [ebp+var_4], 0 ; var_4初始化为0
0040108E push eax
0040108F push offset aD_0 ; "%d"
00401094 call scanf ; 调用scanf函数
00401099 mov eax, [ebp+var_4] ; eax=var_4
0040109C inc eax ; eax = var_4 + 1
0040109D push eax
0040109E push offset aD ; "%d\n"
004010A3 call printf ; 调用printf函数
004010A8 add esp, 10h ; 恢复栈平衡,上面push了4次参数
004010AB xor eax, eax ; eax=0
004010AD mov esp, ebp ; 恢复栈平衡 对上面的push ecx而言
004010AF pop ebp
004010B0 retn ; 函数内没有平衡参数,可以见此函数为__cdecl调用方式

笔记--------------------------------------------end
通过对代码清单6-7的分析,我们学习了函数参数的传递过程,从而理解了C\C++中不定长参数的函数的实现过程。C\C++中不定长参数函数的定义如下。
至少有一个参数。
所有不定长的参数类型传入时都是dword类型。
须在某一个参数中描述参数总个数或将最后一个参数赋值为结尾标记。
有了这3个特性,就可以实现不定参数的函数。根据参数的传递特性,只要确定第一个参数的地址,对其地址值做加法,就可以访问此参数下一个参数所在的地址。获取参数的类型是为了解释地址中的数据。上面提到的第三点是为了获取参数的个数,目的是正确访问到最后一个参数的地址,防止访问参数空间越界(使用栈传参方式的32位程序,某个参数的地址加4即可得到下一个参数所在的地址,double类型除外,详见2.2.1节的介绍)。printf函数就是利用第一个参数获取参数总个数的。只须检查printf函数中第一个参数指向的字符串中包含几个“%”,就可以确定其后的参数个数(“%%”形成的转义字符除外)。
6.5 函数的返回值
函数调用结束后,ret指令执行后为什么可以返回到函数调用处的下一条指令呢?执行call指令时,该指令同时还会做另一件事,那就是将下一条指令所在的地址压入栈中。图6-4为函数调用前esp与栈中内存数据的信息。

图6-4 调用函数前esp与栈中的信息
call指令的下一句指令所在地址为0x008A19E3,当前esp保存的地址为0x009AFB70。当执行call指令时,再次进入函数实现中观察esp与栈数据的变化,发现esp被减4,并且其对应地址中的数据被修改了,如图6-5所示。

图6-5 执行call指令后esp与栈中内存数据的信息
在图6-5中,执行call指令后,由于有压栈操作,esp被减4,修改为0x009AFB6C,并且该地址中保存的信息为0x008A19E3。对比图6-4,该地址即为函数的返回地址。当函数执行到ret指令时,当前esp已经被平衡,此时将再次指向0x009AFB70。函数退出前,会执行ret指令,这个指令取得esp指向的4字节内容作为函数的返回地址值更新eip,程序的流程回到返回地址处,同时执行esp加4的操作,以释放返回的地址空间,平衡栈顶。
前面分析了call和ret指令的细节,介绍了栈结构中函数的运行机制。那么函数的返回值是如何得到的呢?编译器中使用寄存器eax保存函数的返回值,因为32位的eax寄存器只能保存4字节数据,所以大于4字节的数据将使用其他方法保存。通常,eax作为返回值,只有基本数据类型与sizeof(type)小于或等于4的自定义类型(浮点类型除外,详见2.2.1节)。在Debug版下,如果函数有返回值,最后的操作通常为对eax赋值后执行ret指令,如代码清单6-6所示。
代码清单6-6 函数返回值Debug版
// C++ 源码
#include <stdio.h>
int getAddr(int n){
int ret = *(int*)(&n - 1);
return ret; //将返回地址作为返回值
}
int main(int argc, char* argv[]) {
int ret = getAddr(1);
return 0;
}
//x86_vs对应汇编代码讲解
00401020 push ebp
00401021 mov ebp, esp
00401023 push ecx
00401024 push 1
00401026 call sub_401000 ;调用getAddr函数
0040102B add esp, 4
0040102E mov [ebp+var_4], eax
00401031 xor eax, eax
00401033 mov esp, ebp
00401035 pop ebp
00401036 retn
00401000 push ebp
00401001 mov ebp, esp
00401003 push ecx
00401004 lea eax, [ebp+8] ;ebp加法与esp加法原理相同,都是取参数,这里为什
;么是加8呢?在Debug版下进入函数后,首先保存ebp会执行
;push ebp的操作,这样esp将执行压栈减4操作,然后执行
;mov ebp, esp的操作,由于栈顶esp之前被修改,所以ebp
;需要加4调整到最初的栈底位置。因此ebp+4可以得到返回地
;址,ebp+8将会寻址第一个参数。以下代码将第一个参数的
;地址传入eax
00401007 sub eax, 4 ;执行eax自减4操作,执行后eax等价于ebp+4,ebp+4保存了
;函数的返回地址
0040100A mov ecx, [eax] ;取出函数返回地址传入ecx
0040100C mov [ebp-4], ecx ;使用ecx赋值局部变量
0040100F mov eax, [ebp-4] ;取出局部变量数据传入eax,用作函数返回值
00401012 mov esp, ebp
00401014 pop ebp
00401015 retn
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Debug版)
004117B0 push ebp
004117B1 mov ebp, esp
004117B3 sub esp, 0CCh
004117B9 push ebx
004117BA push esi
004117BB push edi
004117BC lea edi, [ebp+var_C]
004117BF mov ecx, 3
004117C4 mov eax, 0CCCCCCCCh
004117C9 rep stosd
004117CB mov ecx, offset unk_41C003
004117D0 call j___CheckForDebuggerJustMyCode
004117D5 push 1
004117D7 call j_getAddr ; 调用getAddr函数
004117DC add esp, 4 ; 栈平衡
004117DF mov [ebp+var_8], eax ; main函数里的局部变量var_8, 即ret = eax
004117E2 xor eax, eax
004117E4 pop edi
004117E5 pop esi
004117E6 pop ebx
004117E7 add esp, 0CCh
004117ED cmp ebp, esp
004117EF call j__RTC_CheckEsp
004117F4 mov esp, ebp
004117F6 pop ebp
004117F7 retn
。。。。。。。
00411740 push ebp
00411741 mov ebp, esp
00411743 sub esp, 0CCh
00411749 push ebx
0041174A push esi
0041174B push edi
0041174C lea edi, [ebp+var_C] ; 关联edi与变量var_C的地址,即ret
0041174F mov ecx, 3
00411754 mov eax, 0CCCCCCCCh
00411759 rep stosd
0041175B mov ecx, offset unk_41C003
00411760 call j___CheckForDebuggerJustMyCode
00411765 lea eax, [ebp+arg_0] ; eax = &arg_0的地址
00411768 sub eax, 4 ; eax = &arg_0 - 4
0041176B mov ecx, [eax] ; ecx = *(int *)(&arg_0 - 1)
0041176D mov [ebp+var_8], ecx ; var_8 = *(int *)(&arg_0 - 1)
00411770 mov eax, [ebp+var_8] ; eax = *(int *)(&arg_0 - 1)
00411773 pop edi
00411774 pop esi
00411775 pop ebx
00411776 add esp, 0CCh
0041177C cmp ebp, esp
0041177E call j__RTC_CheckEsp
00411783 mov esp, ebp
00411785 pop ebp
00411786 retn
。。。。。


笔记--------------------------------------------end
代码清单6-6利用函数的特性,通过对参数地址的间接访问得到函数返回地址,最后通过eax寄存器将其返回。接下来我们分析一个不寻常的示例,返回值类型为自定义类型——结构体,其大小超过4字节,编译器会如何处理呢?请阅读代码清单6-7。
代码清单6-7 结构体类型作为返回值
// C++源码
#include <stdio.h>
struct Test {
int mem1;
int mem2;
};
Test retStruct() {
Test test;
test.mem1 = 1;
test.mem2 = 2;
return test;
}
int main(int argc, char* argv[]) {
Test test;
test = retStruct();
return 0;
}
//x86_vs对应汇编代码讲解
00401020 push ebp
00401021 mov ebp, esp
00401023 sub esp, 10h
00401026 call sub_401000 ;调用retStruct函数
0040102B mov [ebp-8], eax
0040102E mov [ebp-4], edx
00401031 mov eax, [ebp-8]
00401034 mov [ebp-10h], eax
00401037 mov ecx, [ebp-4]
0040103A mov [ebp-0Ch], ecx
0040103D xor eax, eax
0040103F mov esp, ebp
00401041 pop ebp
00401042 retn
00401000 push ebp
00401001 mov ebp, esp
00401003 sub esp, 8
00401006 mov dword ptr [ebp-8], 1 ;对结构体成员变量赋值
0040100D mov dword ptr [ebp-4], 2 ;对结构体成员变量赋值
00401014 mov eax, [ebp-8] ;取结构体成员变量数据传入eax
00401017 mov edx, [ebp-4] ;取结构体成员变量数据传入edx
0040101A mov esp, ebp
0040101C pop ebp
0040101D retn ;执行ret指令,结束函数调用
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Debug版)
00411815 call j_retStruct ; 调用retStruct函数
0041181A mov [ebp+var_DC], eax ; var_DC = var_C = 1
00411820 mov [ebp+var_D8], edx ; var_D8 = var_8 = 2
00411826 mov eax, [ebp+var_DC] ; eax = var_DC
0041182C mov ecx, [ebp+var_D8] ; ecx = var_D8
00411832 mov [ebp+var_C], eax ; var_C = var_DC
00411835 mov [ebp+var_8], ecx ; var_8 = var_D8
00411838 xor eax, eax
0041183A push edx
0041183B mov ecx, ebp
0041183D push eax
0041183E lea edx, dword_411860
00411844 call j__RTC_CheckStackVars
00411849 pop eax
0041184A pop edx
0041184B pop edi
0041184C pop esi
0041184D pop ebx
0041184E add esp, 0E0h
00411854 cmp ebp, esp
00411856 call j__RTC_CheckEsp
0041185B mov esp, ebp
0041185D pop ebp
0041185E retn
........
00411765 mov [ebp+var_C], 1 ; 对结构体成员变量赋值
0041176C mov [ebp+var_8], 2 ; 对结构体成员变量赋值
00411773 mov eax, [ebp+var_C] ; 取结构体成员变量数据传入eax
00411776 mov edx, [ebp+var_8] ; 取结构体成员变量数据传入edx
00411779 push edx
0041177A mov ecx, ebp
0041177C push eax
0041177D lea edx, dword_4117A0
00411783 call j__RTC_CheckStackVars
00411788 pop eax
00411789 pop edx
0041178A pop edi
0041178B pop esi
0041178C pop ebx
0041178D add esp, 0D0h
00411793 cmp ebp, esp
00411795 call j__RTC_CheckEsp
0041179A mov esp, ebp
0041179C pop ebp
0041179D retn


笔记--------------------------------------------end
代码清单6-9演示了一个返回类型为结构体并且大于4字节的返回流程。因为只有两个成员,所以编译器使用了eax和edx传递返回值。本节重点讲解了函数的识别,目的只是让读者了解函数对返回值的处理,关于结构体知识的更多讲解见第9章。
6.6 x64调用约定
x86应用程序的函数调用有__stdcall、__cdecl、__fastcall等方式,但x64应用程序只有1种寄存器快速调用约定。前4个参数使用寄存器传递,如果参数超过4个,多余的参数就放在栈里,参数传递顺序为从右到左,由函数调用方平衡栈空间。前4个参数存放的寄存器是固定的,分别是第1个参数RCX、第2个参数RDX、第3个参数R8、第4个参数R9,其他参数从右往左依次入栈。任何大于8字节或不是1字节、2字节、4字节、8字节的参数通过引用传递。所有浮点参数都是使用XMM寄存器完成传递的,它们在XMM0、XMM1、XMM2和XMM3中传递,如表6-1所示。
表6-1 x64应用程序前4个参数的调用约定

注意,如果参数既有浮点类型又有整数类型,如void fun(float, int,float,int),那么参数传递顺序为第1个参数(XMM0)、第2个参数(RDX)、第3个参数(XMM2)、第4个参数(R9)。
还有一点需要注意,函数的前4个参数虽然使用寄存器传递,但是栈中仍然为这4个参数预留了空间(32字节),为方便描述,这里称之为栈预留空间。因为在x64环境里,前4个参数使用寄存器传递,那么在函数内部,这4个寄存器就不能使用了,所以相当于函数少了4个可用的通用寄存器。当函数功能复杂时,可能造成寄存器不够用的问题。为了避免这个问题,可以使用预留栈空间,方法是函数调用者多申请32字节的栈空间,当函数寄存器不够时,可以把寄存器的值保存到刚才申请的栈空间中。预留栈空间由函数调用者提前申请,也由函数调用者负责平衡栈空间。
函数调用后,寄存器和内存的情况如图6-6所示。

图6-6 函数参数传递寄存器和内存的情况代码
清单6-8演示了x64调用约定的流程。
代码清单6-8 x64调用约定
// C++源码
#include <stdio.h>
//整型传参
int add1(int n1, int n2, int n3, int n4, int n5, int n6) {
return n1 + n2 + n3 + n4 + n5 + n6;
}
//浮点型传参
int add2(float n1, float n2, float n3, float n4, float n5, float n6) {
return n1 + n2 + n3 + n4 + n5 + n6;
}
//混合传参
int add3(int n1, float n2, int n3, float n4, int n5, float n6) {
return n1 + n2 + n3 + n4 + n5 + n6;
}
int main(int argc, char* argv[]) {
add1(1, 2, 3, 4, 5, 6);
add2(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f);
add3(1, 2.0f, 3, 4.0f, 5, 6.0f);
return 0;
}
//x64_vs对应汇编代码讲解
00000001400010E0 mov [rsp+10h], rdx ;保存参数2到栈预留空间
00000001400010E5 mov [rsp+8], ecx ;保存参数1到栈预留空间
00000001400010E9 sub rsp, 38h ;申请栈空间,栈预留空间20h+参
;数空间8h+局部变量空间+栈对齐=38h
;64程序栈对齐值为16B传参需要的
;栈空间,在这里统一申请
00000001400010ED mov dword ptr [rsp+28h], 6 ;参数6,通过入栈传递
00000001400010F5 mov dword ptr [rsp+20h], 5 ;参数5,通过入栈传递
00000001400010FD mov r9d, 4 ;参数4,通过寄存器r9传递
0000000140001103 mov r8d, 3 ;参数3,通过寄存器r8传递
0000000140001109 mov edx, 2 ;参数2,通过寄存器rdx传递
000000014000110E mov ecx, 1 ;参数1,通过寄存器rcx传递
0000000140001113 call sub_140001000 ;调用add1函数
0000000140001118 movss xmm0, cs:dword_14000D2D4
0000000140001120 movss dword ptr [rsp+28h], xmm0 ;参数6,通过入栈传递
0000000140001126 movss xmm0, cs:dword_14000D2D0
000000014000112E movss dword ptr [rsp+20h], xmm0 ;参数5,通过入栈传递
0000000140001134 movss xmm3, cs:dword_14000D2CC ;参数4,通过寄存器xmm3传递
000000014000113C movss xmm2, cs:dword_14000D2C8 ;参数3,通过寄存器xmm2传递
0000000140001144 movss xmm1, cs:dword_14000D2C4 ;参数2,通过寄存器xmm1传递
000000014000114C movss xmm0, cs:dword_14000D2C0 ;参数1,通过寄存器xmm0传递
0000000140001154 call sub_140001040 ;调用add2函数
0000000140001159 movss xmm0, cs:dword_14000D2D4
0000000140001161 movss dword ptr [rsp+28h], xmm0 ;参数6,通过入栈传递
0000000140001167 mov dword ptr [rsp+20h], 5 ;参数5,通过入栈传递
000000014000116F movss xmm3, cs:dword_14000D2CC ;参数4,通过寄存器xmm3传递
0000000140001177 mov r8d, 3 ;参数3,通过寄存器r8传递
000000014000117D movss xmm1, cs:dword_14000D2C4 ;参2,通过寄存器xmm1传递
0000000140001185 mov ecx, 1 ;参数1,通过寄存器rcx传递
000000014000118A call sub_140001090 ;调用add3函数
000000014000118F xor eax, eax ;设置返回值为0
0000000140001191 add rsp, 38h ;释放申请的栈空间
0000000140001195 retn ;执行ret指令结束函数调用
0000000140001000 mov [rsp+20h], r9d ;保存参数4到栈预留空间
0000000140001005 mov [rsp+18h], r8d ;保存参数3到栈预留空间
000000014000100A mov [rsp+10h], edx ;保存参数2到栈预留空间
000000014000100E mov [rsp+8], ecx ;保存参数1到栈预留空间
0000000140001012 mov eax, [rsp+10h] ;eax=n2
0000000140001016 mov ecx, [rsp+8] ;ecx=n1
000000014000101A add ecx, eax ;ecx=n1+n2
000000014000101C mov eax, ecx ;eax=n1+n2
000000014000101E add eax, [rsp+18h] ;eax=n1+n2+n3
0000000140001022 add eax, [rsp+20h] ;eax=n1+n2+n3+n4
0000000140001026 add eax, [rsp+28h] ;eax=n1+n2+n3+n4+n5
000000014000102A add eax, [rsp+30h] ;返回值,eax=n1+n2+n3+n4+n5+n6
000000014000102E retn ;执行ret指令,结束函数调用
0000000140001040 movss dword ptr [rsp+20h], xmm3 ;保存参数4到栈预留空间
0000000140001046 movss dword ptr [rsp+18h], xmm2 ;保存参数3到栈预留空间
000000014000104C movss dword ptr [rsp+10h], xmm1 ;保存参数2到栈预留空间
0000000140001052 movss dword ptr [rsp+8], xmm0 ;保存参数1到栈预留空间
0000000140001058 movss xmm0, dword ptr [rsp+8] ;xmm0=n1
000000014000105E addss xmm0, dword ptr [rsp+10h] ;xmm0=n1+n2
0000000140001064 addss xmm0, dword ptr [rsp+18h] ;xmm0=n1+n2+n3
000000014000106A addss xmm0, dword ptr [rsp+20h] ;xmm0=n1+n3+n3+n4
0000000140001070 addss xmm0, dword ptr [rsp+28h] ;xmm0= n1+n3+n3+n4+n5
0000000140001076 addss xmm0, dword ptr [rsp+30h] ;xmm0= n1+n3+n3+n4+n5+n6
000000014000107C cvttss2si eax, xmm0 ;xmm0=(int)xmm0
0000000140001080 retn ;执行ret指令,结束函数调用
0000000140001090 movss dword ptr [rsp+20h], xmm3 ;保存参数4到栈预留空间
0000000140001096 mov [rsp+18h], r8d ;保存参数3到栈预留空间
000000014000109B movss dword ptr [rsp+10h], xmm1 ;保存参数2到栈预留空间
00000001400010A1 mov [rsp+8], ecx ;保存参数1到栈预留空间
00000001400010A5 cvtsi2ss xmm0, dword ptr [rsp+8] ;xmm0=(float)n1
00000001400010AB addss xmm0, dword ptr [rsp+10h] ;xmm0=n1+n2
00000001400010B1 cvtsi2ss xmm1, dword ptr [rsp+18h] ;xmm1=(float)n3
00000001400010B7 addss xmm0, xmm1 ;xmm0=n1+n2+n3
00000001400010BB addss xmm0, dword ptr [rsp+20h] ;xmm0=n1+n2+n3+n4
00000001400010C1 cvtsi2ss xmm1, dword ptr [rsp+28h] ;xmm1=(float)n5
00000001400010C7 addss xmm0, xmm1 ;xmm0=n1+n2+n3+n4+n5
00000001400010CB addss xmm0, dword ptr [rsp+30h] ;xmm0=n1+n2+n3+n4+n5+n6
00000001400010D1 cvttss2si eax, xmm0 ;返回值eax=(int)xmm0
00000001400010D5 retn ;执行ret指令,结束函数调用
//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x64_vs对应汇编代码讲解 (Debug版)
0000000140011920 mov [rsp-8+arg_8], rdx ; 保存参数2到栈预留空间
0000000140011925 mov [rsp-8+arg_0], ecx ; 保存参数1到栈预留空间
0000000140011929 push rbp
000000014001192A push rdi
000000014001192B sub rsp, 0F8h ; 申请栈空间, 这里如何计算的?
000000014001192B ; 参数空间+局部变量空间+栈对齐
0000000140011932 lea rbp, [rsp+30h]
0000000140011937 lea rcx, unk_140021003
000000014001193E call j___CheckForDebuggerJustMyCode
0000000140011943 mov [rsp+100h+var_D8], 6 ; 参数6,通过入栈传递
000000014001194B mov [rsp+100h+var_E0], 5 ; 参数5,通过入栈传递
0000000140011953 mov r9d, 4 ; 参数4,通过寄存器r9传递
0000000140011959 mov r8d, 3 ; 参数3,通过寄存器r8传递
000000014001195F mov edx, 2 ; 参数2,通过寄存器rdx传递
0000000140011964 mov ecx, 1 ; 参数1,通过寄存器rcx传递
0000000140011969 call j_add1 ; 调用add1函数
000000014001196E movss xmm0, cs:__real@40c00000
0000000140011976 movss [rsp+100h+var_D8], xmm0 ; 参数6,通过入栈传递
000000014001197C movss xmm0, cs:__real@40a00000
0000000140011984 movss [rsp+100h+var_E0], xmm0 ; 参数5,通过入栈传递
000000014001198A movss xmm3, cs:__real@40800000 ; 参数4,通过寄存器xmm3传递
0000000140011992 movss xmm2, cs:__real@40400000 ; 参数3,通过寄存器xmm2传递
000000014001199A movss xmm1, cs:__real@40000000 ; 参数2,通过寄存器xmm1传递
00000001400119A2 movss xmm0, cs:__real@3f800000 ; 参数1,通过寄存器xmm0传递
00000001400119AA call j_add2 ; 调用add2函数
00000001400119AF movss xmm0, cs:__real@40c00000
00000001400119B7 movss [rsp+100h+var_D8], xmm0 ; 参数6,通过入栈传递
00000001400119BD mov [rsp+100h+var_E0], 5 ; 参数5,通过入栈传递
00000001400119C5 movss xmm3, cs:__real@40800000 ; 参数4,通过寄存器xmm3传递
00000001400119CD mov r8d, 3 ; 参数3,通过寄存器r8传递
00000001400119D3 movss xmm1, cs:__real@40000000 ; 参2,通过寄存器xmm1传递
00000001400119DB mov ecx, 1 ; 参数1,通过寄存器rcx传递
00000001400119E0 call j_add3 ; 调用add3函数
00000001400119E5 xor eax, eax ; 设置返回值为0
00000001400119E7 lea rsp, [rbp+0C8h]
00000001400119EE pop rdi
00000001400119EF pop rbp
00000001400119F0 retn
..................
0000000140011760 mov [rsp-8+arg_18], r9d ; 保存参数4到栈预留空间
0000000140011765 mov [rsp-8+arg_10], r8d ; 保存参数3到栈预留空间
000000014001176A mov [rsp-8+arg_8], edx ; 保存参数2到栈预留空间
000000014001176E mov [rsp-8+arg_0], ecx ; 保存参数1到栈预留空间
0000000140011772 push rbp
0000000140011773 push rdi
0000000140011774 sub rsp, 0E8h
000000014001177B lea rbp, [rsp+20h]
0000000140011780 lea rcx, unk_140021003
0000000140011787 call j___CheckForDebuggerJustMyCode
000000014001178C mov eax, [rbp+0D0h+arg_8] ; eax=n2
0000000140011792 mov ecx, [rbp+0D0h+arg_0] ; ecx=n1
0000000140011798 add ecx, eax ; ecx=n1+n2
000000014001179A mov eax, ecx ; eax=n1+n2
000000014001179C add eax, [rbp+0D0h+arg_10] ; eax=n1+n2+n3
00000001400117A2 add eax, [rbp+0D0h+arg_18] ; eax=n1+n2+n3+n4
00000001400117A8 add eax, [rbp+0D0h+arg_20] ; eax=n1+n2+n3+n4+n5
00000001400117AE add eax, [rbp+0D0h+arg_28] ; eax=n1+n2+n3+n4+n5+n6
00000001400117B4 lea rsp, [rbp+0C8h]
00000001400117BB pop rdi
00000001400117BC pop rbp
00000001400117BD retn ; 返回值在eax中
............
00000001400117E0 movss [rsp-8+arg_18], xmm3 ; 保存参数4到栈预留空间
00000001400117E6 movss [rsp-8+arg_10], xmm2 ; 保存参数3到栈预留空间
00000001400117EC movss [rsp-8+arg_8], xmm1 ; 保存参数2到栈预留空间
00000001400117F2 movss [rsp-8+arg_0], xmm0 ; 保存参数1到栈预留空间
00000001400117F8 push rbp
00000001400117F9 push rdi
00000001400117FA sub rsp, 0E8h
0000000140011801 lea rbp, [rsp+20h]
0000000140011806 lea rcx, unk_140021003
000000014001180D call j___CheckForDebuggerJustMyCode
0000000140011812 movss xmm0, [rbp+0D0h+arg_0] ; xmm0=n1
000000014001181A addss xmm0, [rbp+0D0h+arg_8] ; xmm0=n1+n2
0000000140011822 addss xmm0, [rbp+0D0h+arg_10] ; xmm0=n1+n3+n3
000000014001182A addss xmm0, [rbp+0D0h+arg_18] ; xmm0= n1+n3+n3+n4
0000000140011832 addss xmm0, [rbp+0D0h+arg_20] ; xmm0= n1+n3+n3+n4+n5
000000014001183A addss xmm0, [rbp+0D0h+arg_28] ; xmm0= n1+n3+n3+n4+n5+n6
0000000140011842 cvttss2si eax, xmm0 ; eax=(int)xmm0
0000000140011846 lea rsp, [rbp+0C8h]
000000014001184D pop rdi
000000014001184E pop rbp
000000014001184F retn ; 返回值在eax中
............
0000000140011870 movss [rsp-8+arg_18], xmm3 ; 保存参数4到栈预留空间
0000000140011876 mov [rsp-8+arg_10], r8d ; 保存参数3到栈预留空间
000000014001187B movss [rsp-8+arg_8], xmm1 ; 保存参数2到栈预留空间
0000000140011881 mov [rsp-8+arg_0], ecx ; 保存参数1到栈预留空间
0000000140011885 push rbp
0000000140011886 push rdi
0000000140011887 sub rsp, 0E8h
000000014001188E lea rbp, [rsp+20h]
0000000140011893 lea rcx, unk_140021003
000000014001189A call j___CheckForDebuggerJustMyCode
000000014001189F cvtsi2ss xmm0, [rbp+0D0h+arg_0] ; xmm0=(float)n1
00000001400118A7 addss xmm0, [rbp+0D0h+arg_8] ; xmm0=n1+n2
00000001400118AF cvtsi2ss xmm1, [rbp+0D0h+arg_10] ; xmm1=(float)n3
00000001400118B7 addss xmm0, xmm1 ; xmm0=n1+n2+n3
00000001400118BB addss xmm0, [rbp+0D0h+arg_18] ; xmm0=n1+n2+n3+n4
00000001400118C3 cvtsi2ss xmm1, [rbp+0D0h+arg_20] ; xmm1=(float)n5
00000001400118CB addss xmm0, xmm1 ; xmm0=n1+n2+n3+n4+n5
00000001400118CF addss xmm0, [rbp+0D0h+arg_28] ; xmm0=n1+n2+n3+n4+n5+n6
00000001400118D7 cvttss2si eax, xmm0 ; 返回值eax=(int)xmm0
00000001400118DB lea rsp, [rbp+0C8h]
00000001400118E2 pop rdi
00000001400118E3 pop rbp
00000001400118E4 retn ; 执行ret指令,结束函数调用




笔记--------------------------------------------end
6.7 本章小结
本章讨论了函数的内部实现机制。在软件开发过程中,通常以面向对象的方式设计程序结构,然后由程序员实现每个对象的成员函数。编译器产生二进制代码后,面向对象变成了模块化的代码,因此在分析人员眼中,这些都是以函数为单位的代码块,本章的内容是逆向分析的基础,至于对C++中成员函数、虚函数等的分析,将在后续章节逐一展开。

3409

被折叠的 条评论
为什么被折叠?



