[转组第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的自定义类型(浮点数除外)。

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

  

原文地址:https://www.cnblogs.com/nww-570/p/8993064.html

时间: 2024-07-29 18:53:46

[转组第7天] | 函数的工作原理的相关文章

C++学习笔记27,虚函数的工作原理

C++规定了虚函数的行为,但是将实现交给了编译器的作者. 通常,编译器处理虚函数的方法是给每一个对象添加一个隐藏成员.隐藏成员中保存了一个指向函数地址数组的指针. 这个数组称为虚函数表(virtual function table,vtbl).虚函数表中存储了为类对象进行声明的虚函数的地址. 例如:基类对象包含一个指针,该指针指向基类的虚函数表. 派生类对象包含一个指针,该指针指向一个独立的虚函数表.如果派生类提供了虚函数的新定义,虚函数表将保存新的函数地址. 如果派生类没有重新定义虚函数,虚函

函数的工作原理——划分RAM搞不懂啊???

1.看到<21天学会C++>P92的函数工作原理之划分RAM,感觉还是迷迷糊糊,不太明白,进一步查询??? 2.程序启动时,操作系统(如DOS,Windows等)将依据编译器的需求设置各种内存区域. 对于一个C++程序员来说,经常需要关心的是全局名称空间.自由存储器.寄存器.代码空间和堆栈. 3.寄存器:CPU中的一个特殊存储区域,任意给定时刻指向下一行代码代码的寄存器组的寄存器被称为指令指针.指令指针的任务是跟踪接下来将执行哪一行代码. 4.代码空间:代码本身存放在代码空间中,每行代码都被转

关于OnPaint函数的工作原理(很详细,很实用) [转载]

地址:http://blog.csdn.net/foreverhuylee/article/details/21889025 用了两年的VC,其实对OnPaint的工作原理一直都是一知半解.这两天心血来潮,到BBS上到处发帖询问,总算搞清楚了,现在总结一下. 对于窗口程序,一般有个特点:窗口大部分的区域保持不变,只有不分区域需要重新绘制.如果将整个窗口全部刷新的画,就做了许多不必要的工作,因而,MFC采用了一套基于无效区的处理机制.在分析无效区处理之前,我们要明白一个现实,现在的机器还不够牛,如

函数的工作原理

函数的工作借助于栈. 栈在内存中是一块特殊的存储空间,它的存储原则是"先进后出",最先被存储的数据最后被释放. esp被称为栈顶指针,ebp称为栈底指针,通过这两个指针寄存器保存当前栈的起始地址与结束地址. esp与ebp之间所构成的空间便成为栈帧.通常,在VC++中,栈帧中可以寻址的数据有局部变量.函数返回地址.函数参数等.不同的两次函数调用,所形成的栈帧也不同.当由一个函数进入到另一个函数中时,就会针对所调用的函数形成所需的栈空间,形成此函数的栈帧.当这个函数结束调用时,需要清除掉

基于arm的C++反汇编 函数的工作原理

栈帧的形成和关闭 各种调用方式的考擦 使用 fp或sp寻址 函数的参数 与返回值 arm指令中立即数存放位置 gdbserver 调试环境 栈帧的形成和关闭 ??栈在内存中是一块特殊的存储空同, 它的存储原则是"先进后出", 即最先被存储的数据最后被释放, 汇编过程通常使用 push 指令与 POP指令对栈空间执行数据压入和数据弹出操作. ??栈结构在内存中占用一段连续的存储空间, 通过sp与 fp这两个栈指针寄存器(在x86上是esp,ebp)来保存当前栈的起始地址与结束地址(又称为

虚函数列表: 取出方法 // 虚函数工作原理和(虚)继承类的内存占用大小计算 32位机器上 sizeof(void *) // 4byte

#include <iostream> using namespace std; class A { public: A(){} virtual void geta(){ cout << "A:A" <<endl; } virtual void getb(){ cout << "A:B" <<endl; } }; class B :public A{ public: B(){} virtual void g

C++中虚函数工作原理和(虚)继承类…

转载请标明出处,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7883531 一.虚函数的工作原理 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数.典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式.vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl.当

C++中虚函数工作原理

一.虚函数的工作原理 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数.典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式.vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl.当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针. 

python中的函数、生成器的工作原理

1.python中函数的工作原理 def foo(): bar() def bar(): pass python的解释器,也就是python.exe(c编写)会用PyEval_EvalFramEx(c函数)运行foo()函数 首先会创建一个栈帧(stack Frame),在栈帧对象的上下文里面去运行这个字节码. import dis print(dis.dis(foo)) #打印字节码 可以尝试着去打印foo的字节码: 关于字节码的解释: LOAD_GLOBAL:首先导入bar这个函数 CALL