《C++反汇编与逆向分析技术揭秘》学习之第6章 函数的工作原理

阅读本章前,我们先思考两个问题。

函数执行时,程序流程会转到函数体的实现地址,只有遇到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_Cvar_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++中成员函数、虚函数等的分析,将在后续章节逐一展开。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值