[转组第7天] | 函数的工作原理

Posted nww-570

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[转组第7天] | 函数的工作原理相关的知识,希望对你有一定的参考价值。

2018-05-04

《C++反汇编和逆向技术》第六章  函数的工作原理 读书笔记

  debug版本的函数调用:

call    func
func:
    push    ebp    ;保存ebp
    mov    ebp,esp
    sub    esp,40h    ;抬高esp,开辟栈空间
    push    ...    ;保存寄存器
    ...
    pop    ...    ;还原寄存器
    add    esp,40h    ;降低esp,释放局部变量空间
    cmp    ebp,esp    ;检测栈平衡
    call    __chkesp    ;进入栈平衡错误检测函数
    mov    esp,ebp    ;还原esp
    pop    ebp
    ret

  函数__chkesp是Debug编译选项组下独有的函数,用于检测栈平衡。在Debug版下,所有的函数退出时都会使用到这个函数。

  使用了O2选项后,将不会存在栈平衡检查的代码,还可能没有保存环境、使用ebp保存当前栈底等一系列操作,代码将变得简洁而有效。

  【call指令和retn指令】

call指令(子程序调用指令):
段内转移:
    push    eip
    jmp    目标位置
段间转移:
    push    CS
    push    eip
    jmp    目标位置
retn/retf指令:
段内转移跳出retn:
    pop    eip
段间转移跳出retf:
    pop    eip
    pop    CS

  【看一段汇编代码】

mov    ecx,10h    ;设置ecx为0x10
mov    eax,0CCCCCCCCh    ;将局部变量初始化为0CCCCCCCCh
rep    stos    dword ptr [edi]    ;根据ecx的值,将eax值内容以4字节为单位写到edi指向的内存中

  rep指令的目的是重复其上面的指令.ECX的值是重复的次数.一般用于初始化局部变量

  STOS指令的作用是将eax中的值拷贝到目的地址。

 

  各种调用方式的考察:  

  汇编过程中通常用 "ret  xxxx" 来平衡参数所使用的栈空间,当函数的参数为不定参数时,函数自身无法确定参数所使用的栈空间的大小,因此无法由函数自身执行平衡操作,需要此函数的调用者执行平衡操作。为了确定参数的平衡者,以及参数的传递方式,于是有了函数的调用约定。VC++环境下的调用约定有三种:_cdecl、_stdcall、_fastcall。

  * _cdecl: C\C++默认的调用方式,调用方平衡栈,不定参数的函数可以使用。

  *_stdcall: 被调方平衡栈,不定参数的函数无法使用。

  *_fastcall: 寄存器方式传参,被调方平衡栈,不定参数的函数无法使用。

  C语言中经常使用的printf函数就是典型的_cdecl调用方式,由于printf的参数可以有多个,所以只能以_cdecl方式调用。

  当printf函数被多次使用后,对于Debug版,它会在每次调用结束后进行栈平衡操作。而经过O2选项的优化后,会采取复写传播优化,将每次参数平衡的操作进行归并,一次性平衡栈顶指针esp。

  通过分析发现,_cdecl与_stdcall只在参数平衡上有所不同,其余部分都一样。但经过优化后,_cdecl调用方式的函数在同一作用域内多次使用,会在效率上比_stdcall高一点。这是因为_cdecl可以使用复写传播,而_stdcall都在函数内平衡参数,无法使用复写传播这种优化方式。

  这三种调用方式中,_fastcall调用方式的效率最高。因为只有它可以利用寄存器(ecx,edx)传递参数。但是要预留参数对应栈空间,为了防止传递参数过程中,寄存器需要接受其他的值而导致参数无法传递(比如上述局部变量初始化,利用了ecx)。这里与_cdecl和_stdcall栈传参不同,_fatscall传参ecx,edx拷贝到预留栈空间是局部变量空间[ebp-xx],而_cdecl和_sdtcall的传递的参数在返回地址之下[ebp+xx]

  【64位平台下栈区空间开辟:与通过push和pop指令在堆栈显式添加和溢出参数的x86编译器不同,x64代码生成器会预留足够大的堆栈空间,以调用最大目标函数(参数方法)所使用的任何内容,随后,在调用子函数时,它重复使用相同的堆栈区域来设置参数。】

  

  使用ebp或esp寻址:

  在大多数情况下,使用ebp寻址局部变量只能在非O2选项中产生,这样做是为了方便调试和检测栈平衡,使目标代码可读性更高。

  而在O2编译选项中,为了提升程序的效率,省去了这些检测工作,在用户编写的代码中,只要栈顶是稳定的,就可以不再使用ebp,而利用esp直接访问局部变量,可以节省一个寄存器资源。如:

lea    eax, [esp + 8 + var_4] ;其中esp + 8等价于ebp.

  

  函数的参数:

  C\C++将不定长参数的函数定义为:

  * 至少要有一个参数。

  * 所有不定长的参数类型传入时都是dword类型。//其实我认为应该是任何参数传入都不会小于dword,push指令每次都会传入一个运算字长,即32位或64位。

  * 需在某一个参数中描述参数总个数或将对后一个参数赋值为结尾标记。

 

  函数的返回值:

  VC中使用寄存器eax来保存返回值,由于32位的eax寄存器只能保存4字节数据,因此大于4字节的数据将使用其他方法保存。通常,eax作为返回值,只有基本类型与sizeof(type)小于等于4的自定义类型(浮点数除外)。

  返回一个结构体类型,且结构体类型内部全是基本类型,则会用寄存器返回,返回后存到调用者的局部变量中。(这个后面章节再分析,不全)。

 

  

以上是关于[转组第7天] | 函数的工作原理的主要内容,如果未能解决你的问题,请参考以下文章

[转组第5天] | 天枢分享Reverse入门

[转组第8天] | 变量在内存中的位置和访问方式

[转组第10天] | Android6.0.0_r1源码编译和POC程序的编译

[转组第9天] | 数组和指针的寻址

[转组第2天] | baby_mips和android xss的调研

金币(NOIP2015 普及组第一题)