网上看到这篇《计算机是如何实现函数调用的》文章,以一个C程序的函数调用为例,非常详细的讲解了在汇编代码级别函数调用是怎么被实现的。
不过在这篇文章中,关于retn 8这条指令个人认为作者讲解不太清楚,理解上有点出入。
作者原文部分:
retn 8 ;ESP+8->396 这里retn是由系统调用的 我们不用管 系统会自动把EIP指针指向 原来的call的下一条指令
我个人的理解是:
retn 8 系统先出栈ESP=388+4=392中得到在函数调用时压入栈位置388中保存的下条指令地址,把这个下条指令地址交给EIP寄存器,然后再ESP=392+8=400,把函数调用时压入栈的两个参数出栈,把栈恢复到函数调用前。
这样CPU就可以从EIP指令寄存器中得到下条指令继续执行下去。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>强力插入原文分割线>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
原文地址: http://www.nowamagic.net/program/program_HowComputerRealizeFunctionCall.php
计算机是如何实现函数调用的
有时候,我们需要深入了解编程语言的一些细节性问题,比如,编程语言结构--函数是如何实现的,函数的执行会是怎么样的一个过程。下面我们举一个例子,看看函数调用的时候,堆栈会发生怎么样的变化。
#include <stdio.h>
long test(int a,int b)
{
a = a + 1;
b = b + 100;
return a + b;
}
void main()
{
printf("%d",test(1000,2000));
}
写成32位汇编就是这样:
;//////////////////////////////////////////////////////////////////////////////////////////////////////
.386
.model flat,stdcall ;这里我们用stdcall 就是函数参数 压栈的时候从最后一个开始压,和被调用函数负责清栈
option casemap:none ;区分大小写
includelib msvcrt.lib ;这里是引入类库 相当于 #include<stdio.h>了
printf PROTO C:DWORD,:VARARG ;这个就是声明一下我们要用的函数头,到时候 汇编程序会自动到msvcrt.lib里面找的了
;:VARARG 表后面的参数不确定 因为C就是这样的printf(const char *, ...);
;这样的函数要注意 不是被调用函数负责清栈 因为它本身不知道有多少个参数
;而是有调用者负责清栈 下面会详细说明
.data
szTextFmt BYTE '%d',0 ;这个是用来类型转换的,跟C的一样,字符用字节类型
a dword 1000 ;假设
b dword 2000 ;处理数值都用双字 没有int 跟long 的区别
;/////////////////////////////////////////////////////////////////////////////////////////
.code
_test proc ;A:DWORD,B:DWORD
push ebp
mov ebp,esp
mov eax,dword ptr ss:[ebp+8]
add eax,1
mov edx,dword ptr ss:[ebp+0Ch]
add edx,100
add eax,edx
pop ebp
retn 8
_test endp
_main proc
push dword ptr ds:b ;反汇编我们看到的b就不是b了而是一个[*****]数字 dword ptr 就是我们在ds(数据段)把[*****]
;开始的一个双字长数值取出来
push dword ptr ds:a ;跟她对应的还有 byte ptr ****就是取一个字节出来 比如这样 mov al,byte ptr ds:szTextFmt
;就把 % 取出来 而不包括 d
call _test
push eax ;假设push eax的地址是×××××
push offset szTextFmt
call printf
add esp,8
ret
_main endp
end _main
;////////////////////////////////////////////////////////////// 下面介绍堆栈的变化
</stdio.h>
首先要明白的是操作堆栈段, ss 只能用 esp或ebp寄存器 其他的寄存器eax ebx edx等都不能够用。而esp永远指向堆栈栈顶,ebp用来在堆栈段里面寻址。
push 指令是压栈 ESP=ESP-4,pop 指令是出栈 ESP=ESP+4。
我们假设main函数一开始堆栈定是 ESP=400。
push dword ptr ds:b ;ESP-4=396 ->里面的值就是 2000 就是b的数值
push dword ptr ds:a ;ESP-4=392 ->里面的值就是 1000 就是a的数值
call test ;ESP-4=388->里面的数值是什么?这个太重要了 就是我们用来找游戏函数的原理所在。
里面的数值就是call test 指令下一条指令的地址->即push eax的地址×××××
到了test函数里面
push ebp ;ESP-4=384->里面保存了当前ebp的值 而不是把ebp清零
mov ebp,esp ;这里ESP=384就没变化了,但是 ebp=esp=384,为什么要这样做呢 因为我们要用ebp到堆栈里面找参数
mov eax,dword ptr ss:[ebp+8] ;反汇编是这样的 想想为什么a就是[ebp+8]呢
;我们往上看看堆栈里地址392处就保存着a的值 这里ebp=384 加上8正好就是392了
;这样就把传递过来的1000拿了出来eax=1000
add eax,1 ;相当于 a+1了 eax=1001
mov edx,dword ptr ss:[ebp+0Ch] ; 0Ch=12 一样道理这里指向堆栈的地址是384+12=396 就是2000了 edx=2000
add edx,100 ;相当于 b+100 edx=2100
add eax,edx ;eax=eax+edx=1001+2100=3101 这里eax已经保存了最终的结果了
;因为win32汇编一般用eax返回结果 所以如果最终结果不是在eax里面的话 还要把它放到eax
;比如假设我的结果保存在变量nRet里面 最后还是要这样 mov eax,dword ptr nRet
pop ebp ;ESP=384+4=388 而保存在栈顶384的值 保存到 ebp中 即恢复ebp原来的值
;因为一开始我们就把ebp的值压栈了,mov ebp,esp已经改变了ebp的值,这里恢复就是保证了堆栈平衡
retn 8 ;ESP+8->396 这里retn是由系统调用的 我们不用管 系统会自动把EIP指针指向 原来的call的下一条指令
;由于是系统自动恢复了call那里的压栈所以 真正返回到的时候ESP+4就是恢复了call压栈的堆栈
;到了这个时候 ESP=400 就是函数调用开始的堆栈,就是说函数调用前跟函数调用后的堆栈是一样的
;这就是堆栈平衡
由于我们用stdcall上面retn 8就是被调用者负责恢复堆栈的意思了,函数test是被调用者,所以负责把堆栈加8,call 那里是系统自动恢复的
push eax ;ESP-4=396->里面保存了eax的值3101
;上面已经看到了eax保存着返回值,我们要把它传给printf也是通过堆栈传递
push offset szTextFmt ;ESP-4=392->里面保存了szTextFmt的地址 也就是C里面的指针 实际上没有什么把字符串传递的,我们传的都是地址
;无论是在汇编或C 所以在汇编里没有什么字符串类型 用最多的就是DWORD。嘿嘿游戏里面传递参数 简单多了
call printf ;ESP-4=388->里面保存了下一条指令的地址
add esp,8 ;ESP+8=400 恢复了调用printf前的堆栈状态
;上面说了由于printf后面参数是:VARARG 这样的类型是有调用者恢复堆栈的 所以printf里面没有retn 8之类的指令
;这是由调用者负责清栈 main是调用者 所以下面一句就是 add esp,8 把堆栈恢复到调用printf之前
;而call printf那里的压栈 是由系统做的 恢复的工作也是系统完成 我们不用理 只是知道里面保存是返回地址就够了
ret ;main 函数返回 其他的事情是系统自动搞定 我们不用理 任务完成
分享到:
相关推荐
在编程世界中,函数调用是程序执行流程中的核心部分,它使得代码模块化,易于理解和维护。本文将深入探讨函数调用过程中涉及的关键概念:堆栈帧、函数调用和堆栈切换。 首先,我们要理解堆栈帧(Stack Frame)。在...
总结来说,系统调用与系统函数调用是软件与硬件、用户程序与操作系统之间沟通的桥梁,它们在计算机系统中起着至关重要的作用。深入理解和熟练运用这些调用,对于提升编程技能和解决复杂问题具有重要意义。
在计算机程序设计中,函数调用是最基本的编程单元。函数调用会在内存中创建一个新的堆栈帧,该堆栈帧用于存放函数的实参、局部变量和返回地址等信息。在这个过程中,main 函数如何调用子函数是非常关键的。今天,...
在计算机科学中,函数调用是程序执行过程中不可或缺的一部分,它允许我们把一系列相关的操作封装成独立的功能单元,便于重复使用和模块化编程。当我们谈论“二进制讲解函数调用”时,实际上是在探讨在处理器级别上,...
该方法利用Python的AST作为分析基础,解析并提取函数调用关系,构建函数调用关系模型,然后生成Python程序的函数调用路径。 该方法的主要步骤包括:首先,对Python代码进行抽象语法树的分析;其次,解析和提取抽象...
函数递归调用堆栈分析是指在计算机科学中,函数递归调用时,函数调用自身的过程中,如何使用堆栈来存储变量和参数的过程。堆栈是一种 lasts-in-first-out(LIFO)的数据结构,用于存储函数调用的参数和变量。 在...
在计算机编程领域,函数调用是一项基本操作,而如何管理函数调用过程中产生的堆栈则是确保程序正确执行的关键之一。本文将深入探讨与`chkesp`相关的函数调用方式,特别是其在堆栈管理和函数调用约定中的应用。 ####...
C通过运行时堆栈支持递归函数的实现。递归函数就是直接或间接调用自身的函数。 许多教科书都把计算机阶乘和菲波那契数列用来说明递归,非常不幸我们可爱的著名的老潭老师的《C语言程序设计》一书中就是从阶乘的计算...
### 使用调用外部程序函数实现API函数高级功能 在计算机编程领域中,API(应用程序接口)是一组定义软件组件如何相互交互的规则。通过API,开发者可以利用预先编写好的功能代码,无需了解底层实现细节就能完成特定...
在C语言中,这一机制通过栈的数据结构实现,栈的特性是后进先出(LIFO),非常适合用于保存函数调用的参数和局部变量。 调用约定是指函数调用和返回时的规范,包括参数的传递顺序、谁负责清理堆栈等问题。文档中...
### PB-API函数调用参考:PowerBuilder 9.0中的打印控制 #### 6.1 系统缺省打印机的获取和设置 在探讨PowerBuilder 9.0中如何进行打印控制之前,需要理解计算机系统可能连接有多个本地或网络打印机,但其中只有一...
程序设计是计算机科学的核心内容之一,函数调用是程序设计中最基本也是最重要的概念之一。函数调用是指在程序中调用已经定义好的函数,以实现特定的功能。本节我们将详细介绍函数调用相关的知识点。 一、函数调用...
在计算机科学中,特别是在软件调试和逆向工程领域,理解函数调用堆栈是至关重要的。函数调用堆栈(也称为调用栈或执行栈)记录了程序中函数调用的顺序,这对于追踪代码执行流程,特别是错误定位非常有用。在x86架构...
本文主要探讨了C/C++语言中函数之间的参数传递机制以及计算机如何处理函数调用中的参数,即函数调用约定。文章重点分析了值传递与地址传递这两种主要的参数传递方式,并讨论了不同情况下的最佳实践。 #### 关键词 ...
### 函数调用与堆栈 #### 变量的生存期概述 在计算机编程中,尤其是在C/C++这类语言中,变量的生存期是指变量在程序执行过程中占据内存的时间段。根据变量生存期的不同,我们可以将变量大致分为三类:静态生存期...
1. STN函数调用简图:这里提到的“STN”很可能是某种特定函数调用关系的简写,但未在文段中具体说明其含义。在IT领域中,“STN”可能代表状态转移网络(State Transition Network),一种用于描述系统状态变化的模型...
在C语言中,函数调用是一个复杂的操作,涉及到一系列的汇编指令来实现。 首先,当我们调用一个函数时,比如`fun(0x8899, 0x1100)`,编译器会生成相应的汇编代码。在示例中,可以看到`push`指令用于将参数压入堆栈,...
通过使用Delphi的RPC库,开发者可以轻松地创建服务端和客户端,定义接口,实现跨进程甚至跨网络的函数调用。 **RPC项目创建步骤** 1. **服务端开发**:首先,在Delphi中创建一个新的服务项目,然后定义需要暴露的...
函数调用是程序中使用函数的关键步骤。一般调用形式包括传递参数、调用函数体执行、返回结果等过程。学生需要掌握如何正确地传递参数,理解传值调用和传址调用的区别,以及如何处理函数返回值。 学习目标包括: 1. ...
使用汇编语言对C语言函数调用方法求平均数 在计算机编程中,汇编语言和C语言都是常用的编程语言。汇编语言是一种低级语言,它可以直接控制计算机硬件,具有高效、灵活和可靠的特点。但是,汇编语言的编程难度较高,...