- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
函数是任何一门高级语言中必须要存在的,使用函数式编程可以让程序可读性更高,充分发挥了模块化设计思想的精髓,今天我将带大家一起来探索函数的实现机理,探索编译器到底是如何对函数这个关键字进行实现的,并使用汇编语言模拟实现函数编程中的参数传递调用规范等.
说到函数我们必须要提起调用约定这个名词,而调用约定离不开栈的支持,栈在内存中是一块特殊的存储空间,遵循先进后出原则,使用push与pop指令对栈空间执行数据压入和弹出操作。栈结构在内存中占用一段连续存储空间,通过esp与ebp这两个栈指针寄存器来保存当前栈起始地址与结束地址,每4个字节保存一个数据.
当栈顶指针esp小于栈底指针ebp时,就形成了栈帧,栈帧中可以寻址的数据有局部变量,函数返回地址,函数参数等。不同的两次函数调用,所形成的栈帧也不相同,当由一个函数进入另一个函数时,就会针对调用的函数开辟出其所需的栈空间,形成此函数的独有栈帧,而当调用结束时,则清除掉它所使用的栈空间,关闭栈帧,该过程通俗的讲叫做栈平衡。而如果栈在使用结束后没有恢复或过度恢复,则会造成栈的上溢或下溢,给程序带来致命错误.
一般情况下在Win32环境默认遵循的就是STDCALL,而在Win64环境下使用的则是FastCALL,在Linux系统上则遵循SystemV的约定,这里我整理了他们之间的异同点. 。
首先先来写一段非函数版的堆栈使用案例,案例中模拟了编译器如何生成Main函数栈帧以及如何对栈帧初始化和使用的流程,笔者通过自己的理解写出了Debug版本的一段仿写代码.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
main PROC
push ebp ; 保存栈底指针ebp
mov ebp,esp ; 调整当前栈底指针到栈顶
sub esp,0e4h ; 抬高栈顶esp开辟局部空间
push ebx ; 保存寄存器
push esi
push edi
lea edi,dword ptr [ ebp - 0e4h ] ; 取出当前函数可用栈空间首地址
mov ecx,39h ; 填充长度
mov eax,0CCCCCCCCh ; 填充四字节数据
rep stosd ; 将当前函数局部空间填充初始值
; 使用当前函数可用局部空间
xor eax,eax
mov dword ptr [ ebp - 08h ],1
mov dword ptr [ ebp - 014h ],2 ; 使用局部变量
mov dword ptr [ ebp - 020h ],3
mov eax,dword ptr [ ebp - 014h ]
add eax,dword ptr [ ebp - 020h ]
; 如果指令影响了堆栈平衡,则需要平栈
push 4 ; 此情况,由于入栈时没有修改过,平栈只需add esp,12
push 5
push 6 ; 如果代码没有自动平栈,则需要手动平
add esp,12 ; 每个指令4字节 * 多少条影响
push 10
push 20
push 30 ; 使用3条指令影响堆栈
pop eax
pop ebx ; 弹出两条
add esp,4 ; 修复堆栈时只需要平一个变量
pop edi ; 恢复寄存器
pop esi
pop ebx
add esp,0e4h ; 降低栈顶esp开辟的局部空间,局部空间被释放
cmp ebp,esp ; 检测堆栈是否平衡,不平衡则直接停机
jne error
pop ebp
mov esp,ebp ; 恢复基址指针
int 3
error:
int 3
main ENDP
END main
CDECL是C/C++中的一种默认调用约定(调用者平栈)。这种调用方式规定函数调用者在将参数压入栈中后,再将控制权转移到被调用函数,被调用函数通过栈顶指针ESP来访问这些参数。函数返回时,由调用者程序负责将堆栈平衡清除。CDECL调用约定的特点是简单易用,但相比于其他调用约定,由于栈平衡的操作需要在函数返回后再进行,因此在一些情况下可能会带来一些性能上的开销.
该调用方式在函数内不进行任何平衡参数操作,而是在退出函数后对esp执行加4操作,从而实现栈平衡。该约定会采用 复写传播 优化,将每次参数平衡的操作进行归并,在函数结束后一次性平衡栈顶指针esp,且不定参数函数也可使用此约定.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
function PROC
push ebp
mov ebp,esp
sub esp,0cch
push ebx
push esi
push edi
lea edi,dword ptr [ ebp - 0cch ] ; 初始化局部变量
mov ecx,33h
mov eax,0CCCCCCCCh
rep stosd
mov eax,dword ptr [ ebp + 08h ] ; 第一个变量(传入参数1)
add eax,dword ptr [ ebp + 0Ch ] ; 第二个变量(传入参数2)
add eax,dword ptr [ ebp + 10h ] ; 第三个变量(传入参数3)
mov dword ptr [ ebp - 08h ],eax ; 将结果放入到局部变量
mov eax,dword ptr [ ebp - 08h ] ; 给eax寄存器返回
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
function endp
main PROC
; 单独调用并无优势
push 3
push 2
push 1
call function ; __cdecl functin(1,2,3)
add esp,12
; 连续调用则可体现出优势
push 5
push 4
push 3
call function ; __cdecl function(3,4,5)
mov ebx,eax
push 6
push 7
push 8
call function ; __cdecl function(8,7,6)
mov ecx,eax
add esp,24 ; 一次性平两次栈
int 3
main ENDP
END main
STDCALL 调用约定规定由被调用者负责将堆栈平衡清除。STDCALL是一种被调用者平栈的约定,这意味着,在函数调用过程中,被调用函数使用栈来存储传递的参数,并在函数返回之前移除这些参数,这种方式可以使调用代码更短小简洁。STDCALL与CDECL只在参数平衡上有所不同,其余部分都一样,但该约定不定参数函数无法使用.
通过以上分析发现 _cdecl 与 _stdcall 两者只在参数平衡上有所不同,其余部分都一样,但经过优化后 _cdecl 调用方式的函数在同一作用域内多次使用,会在效率上比 _stdcall 髙,这是因为 _cdecl 可以使用复写传播优化,而 _stdcall 的平栈都是在函数内部完成的,无法使用复写传播这种优化方式.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
function PROC
push ebp
mov ebp,esp
sub esp,0cch
push ebx
push esi
push edi
lea edi,dword ptr [ ebp - 0cch ] ; 初始化局部变量
mov ecx,33h
mov eax,0CCCCCCCCh
rep stosd
mov eax,dword ptr [ ebp + 08h ] ; 第一个变量(传入参数1)
add eax,dword ptr [ ebp + 0Ch ] ; 第二个变量(传入参数2)
add eax,dword ptr [ ebp + 10h ] ; 第三个变量(传入参数3)
mov dword ptr [ ebp - 08h ],eax ; 将结果放入到局部变量
mov eax,dword ptr [ ebp - 08h ] ; 给eax寄存器返回
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 12 ; 应用stdcall时,通过ret对目标平栈
function endp
main PROC
push 3
push 2
push 1
call function ; __stdcall functin(1,2,3)
mov ebx,eax ; 获取返回值
push 4
push 5
push 6
call function ; __stdcall function(6,5,4)
mov ecx,eax ; 获取返回值
add ebx,ecx ; 结果相加
int 3
main ENDP
END main
FASTCALL是一种针对寄存器的调用约定。它通常采用被调用者平衡堆栈的方式,类似于STDCALL调用约定。但是,FASTCALL约定规定函数的前两个参数在ECX和EDX寄存器中传递,节省了压入堆栈所需的指令。此外,函数使用堆栈来传递其他参数,并在返回之前使用类似于STDCALL约定的方式来平衡堆栈.
FASTCALL的优点是可以在发生大量参数传递时加快函数的处理速度,因为使用寄存器传递参数比使用堆栈传递参数更快。但是,由于FASTCALL约定使用的寄存器数量比CDECL和STDCALL约定多,因此它也有一些限制,例如不支持使用浮点数等实现中需要使用多个寄存器的数据类型.
FASTCALL效率最高,其他两种调用方式都是通过栈传递参数,唯独 _fastcall 可以利用寄存器传递参数,一般前两个或前四个参数用寄存器传递,其余参数传递则转换为栈传递,此约定不定参数函数无法使用.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
function PROC
push ebp
mov ebp,esp
sub esp,0e4h
push ebx
push esi
push edi
push ecx
lea edi,dword ptr [ ebp - 0e4h ] ; 初始化局部变量
mov ecx,39h
mov eax,0CCCCCCCCh
rep stosd
pop ecx
mov dword ptr [ ebp - 14h ],edx ; 读入第二个参数放入局部变量
mov dword ptr [ ebp - 8h ],ecx ; 读入第一个参数放入局部变量
mov eax,dword ptr [ ebp - 8h ] ; 从局部变量内读入第一个参数
add eax,dword ptr [ ebp - 14h ] ; 从局部变量内读入第二个参数
add eax,dword ptr [ ebp + 8h ] ; 从堆栈中读入第三个参数
add eax,dword ptr [ ebp + 0ch ] ; 从堆栈中读入第四个参数
add eax,dword ptr [ ebp + 10h ] ; 从堆栈中读入第五个参数
add eax,dword ptr [ ebp + 14h ] ; 从堆栈中读入第六个参数
mov dword ptr [ ebp - 20h ],eax ; 将结果给第三个局部变量
mov eax,dword ptr [ ebp - 20h ] ; 返回数据
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 16 ; 平栈
function endp
main PROC
push 6
push 5
push 4
push 3
mov edx,2
mov ecx,1 ; __fastcall function(1,2,3,4,5,6)
call function ; 调用函数
int 3
main ENDP
END main
编译器开启了O2优化模式选项,则为了提高程序执行效率,只要栈顶是稳定的,编译器编译时就不再使用ebp指针了,而是利用esp指针直接访问局部变量,这样可节省一个寄存器资源.
在程序编译时编译器会自动为我们计算ESP基地址与传入变量的参数偏移,使用esp寻址后,不必每次进入函数后都调整栈底ebp,从而减少了ebp的使用,因此可以有效提升程序执行效率。但如果在函数执行过程中esp发生了变化,再次访问变量就需要重新计算偏移了.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
function PROC
push ebp
mov ebp,esp
sub esp,0ch
push esi
; 动态计算出四个参数
lea eax,dword ptr [ esp - 4h + 01ch ] ; 计算参数1 [esp+18]
lea ebx,dword ptr [ esp - 0h + 01ch ] ; 计算参数2 [esp+1c]
lea ecx,dword ptr [ esp + 4h + 01ch ] ; 计算参数3 [esp+20]
lea edx,dword ptr [ esp + 8h + 01ch ] ; 计算参数4 [esp+24]
; 如果ESP被干扰则需要动态调整
lea eax,dword ptr [ esp - 4h + 01ch ] ; 当前参数1的地址
push ebx
push ecx ; 指令让ESP被减去8
lea eax,dword ptr [ esp - 4h + 01ch + 8h ] ; 此处需要+8h修正堆栈
add esp,0ch
pop esi
mov esp,ebp
pop ebp
ret
function endp
main PROC
push 5
push 3
push 4
push 1
call function
int 3
main ENDP
END main
这里我们以一维数组为例,二维数组的传递其实和一维数组是相通的,只不过在寻址方式上要使用二维数组的寻址公式,此外传递数组其实本质上就是传递指针,所以数组与指针的传递方式也是相通的.
使用汇编仿写数组传递方式,在 main 函数内我们动态开辟一块栈空间,并将数组元素依次排列在栈内,参数传递时通过 lea eax,dword ptr [ ebp - 18h ] 获取到数组栈地址空间,由于main函数并不会被释放所以它的栈也是稳定的,调用 function 函数时只需要将栈首地址通过 push eax 的方式传递给 function 函数内,并在函数内通过 mov ecx,dword ptr [ ebp + 8h ] 获取到函数基地址,通过比例因子定位栈空间.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
function PROC
push ebp
mov ebp,esp
sub esp,0cch
push ebx
push esi
push edi
lea edi,dword ptr [ ebp - 0cch ]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stosd
; 检索数组第一个元素
mov eax,1
mov ecx,dword ptr [ ebp + 8h ] ; 定位数组基地址
mov edx,dword ptr [ ecx + eax * 4 ] ; 定位元素
; 检索数组第二个元素
mov eax,2
mov ecx,dword ptr [ ebp + 8h ]
mov edx,dword ptr [ ecx + eax * 4 ]
pop edi
pop esi
pop ebx
add esp,0cch
mov esp,ebp
pop ebp
ret
function ENDP
main PROC
push ebp
mov ebp,esp
sub esp,0dch
push ebx
push esi
push edi
lea edi,dword ptr [ ebp - 0dch ]
mov ecx,37h
mov eax,0CCCCCCCCh
rep stosd
mov dword ptr [ ebp - 18h ],1 ; 局部空间存储数组元素
mov dword ptr [ ebp - 14h ],2
mov dword ptr [ ebp - 10h ],3
mov dword ptr [ ebp - 0ch ],4
mov dword ptr [ ebp - 8h ],5
push 5
lea eax,dword ptr [ ebp - 18h ] ; 取数组首地址并入栈
push eax
call function ; 调用函数 function(5,eax)
add esp,8 ; 平栈
pop edi
pop esi
pop ebx
add esp,0dch
mov esp,ebp
pop ebp
ret
main ENDP
END main
程序通过CALL指令跳转到函数首地址执行代码,既然是地址那就可以使用指针变量来存储函数的首地址,该指针变量被称作函数指针.
在编译时编译器为函数代码分配一段存储空间,这段存储空间的起始地址就是这个函数的指针,我们可以调用这个指针实现间接调用指针所指向的函数.
#include <iostream>
void __stdcall Show(int x, int y)
{
printf("%d --> %d \n",x,y);
}
int __stdcall ShowPrint(int nShow, int nCount)
{
int ref = nShow + nCount;
return ref;
}
int main(int argc, char* argv[])
{
// 空返回值调用
void(__stdcall *pShow)(int,int) = Show;
pShow(1,2);
// 带参数调用返回
int(__stdcall *pShowPrint)(int, int) = ShowPrint;
int Ret = pShowPrint(2, 4);
printf("返回值 = %d \n", Ret);
return 0;
}
首先我们使用汇编仿写 ShowPrint 函数以及该函数所对应的 int(__stdcall *pShowPrint)(int, int) 函数指针,看一下在汇编层面该如何实现这个功能.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
function PROC
push ebp
mov ebp,esp
sub esp,0cch
push ebx
push esi
push edi
lea edi,dword ptr [ ebp - 0cch ]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stosd
mov eax,dword ptr [ ebp + 4h ] ; 此处+4得到的是返回后上一条指令地址
mov eax,dword ptr [ ebp + 8h ] ; 得到第一个堆栈传入参数地址
mov ebx,dword ptr [ ebp + 0ch ] ; 得到第二个堆栈传入参数地址
add eax,ebx ; 递增并返回到EAX
pop edi
pop esi
pop ebx
add esp,0cch
mov esp,ebp
pop ebp
ret
function ENDP
main PROC
push ebp
mov ebp,esp
sub esp,0d8h
push ebx
push esi
push edi
lea edi,dword ptr [ ebp - 0d8h ]
mov ecx,36h
mov eax,0CCCCCCCCh
rep stosd
lea eax,function ; 获取函数指针
mov dword ptr [ ebp - 8h ],eax ; 将指针放入局部空间
push 4
push 2 ; 传入参数
call dword ptr [ ebp - 8h ] ; 调用函数
add esp,8 ; 平栈
pop edi
pop esi
pop ebx
add esp,0d8h
mov esp,ebp
pop ebp
ret
main ENDP
END main
本文作者: 王瑞 本文链接: https://www.lyshark.com/post/17fb1a42.html 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处! 。
最后此篇关于5.5汇编语言:函数调用约定的文章就讲到这里了,如果你想了解更多关于5.5汇编语言:函数调用约定的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
假设我有一个函数,例如 f(x,y)但y参数是可选的。设置y的首选方式是什么?作为可选参数?对我有用的一种选择: function f(x, y=nothing) # do stuff
在学习核心动画时,我很快就了解到,如果你做得不对,你会得到非常奇怪的未定义行为。为此,我有几个问题可以帮助我从概念上更好地理解它。 我的 NSView 子类在其 init.h 中声明以下内容:该 Vi
我一直在尝试学习 ClojureScript,并偶然发现了一些非常神秘的函数名称。 例如: (.-length str) 来自 om 文档: (defn add-contact [data owner
我对此很好奇。我最近想到使用大括号来隔离代码段,以进行可视化组织,并将变量隔离到特定范围(如果只是为了防止它们在较大的函数中混淆 Eclipse 中的建议)。例如: public void start
所以我进行了一些搜索,但在 Google 或 PEP 上找不到任何讨论此问题的内容。 我正在使用 tkinter 做一个项目,我有一个文件,它是项目的一部分,只有 200 行代码(不包括所有注释掉的代
根据某种不成文的约定,所有 API key 都是十六进制数字吗? 最佳答案 对一些半随机数据(例如时间戳 + 用户 ID + key )进行 md5 哈希是一种快速生成难以猜测的固定长度 key 的方
在 C 中,如果编译器想要提供实现定义的标识符(语言扩展、内在函数、伪函数和伪宏,基本上任何不是语言标准保留关键字但也不是常规函数的东西)惯例是名称以下划线为前缀;据了解,代码不应出于正常目的使用此类
Java Bean 的约定之一是: setter 的返回类型必须是 void。 或者至少大多数人是这么说的。我的问题是:它真的必须无效吗?我喜欢返回 "this" 而不是 "void" 因为我可以像这
我对 Java 编码风格有疑问。 我编码: public class Pattern { public Pattern(...) { ... } public static List
我有一个关于 Java servlet 约定的问题。在查看 servlet 的任何教程时,无论是 Eclipse、NetBeans 等,它们总是让您创建一个 index.jsp 页面。创建页面后,他们
在 python 中,当变量名与保留字冲突时(如 class、in、default 等),PEP8约定规定应使用尾随下划线(class_、in_、default_)。 对于相同的情况,共享最多的 ja
关闭。这个问题是opinion-based .它目前不接受答案。 想要改进这个问题? 更新问题,以便 editing this post 可以用事实和引用来回答它. 关闭 5 年前。 Improve
Java 存储预定义字符串集的主要约定是什么?现在我有一个包含我使用的所有字符串的类,但感觉确实有更好的方法来做到这一点。 这就是我在 my_strings.java 类中所做的 public fin
我经常需要检索结果并按给定列访问它们。 有没有一种方法可以不用每次都遍历整个数据集来编写它? 我研究了各种 PDO 获取模式,但没有发现比这更简单的模式。谢谢。 function get_groups
所以我正在制作这个 android 应用程序,它需要从用户提供的 CSV 文件中读取数据。 CSV 文件在台式电脑上编辑起来更舒服,所以我在应用程序中没有编辑器,它是“只读”的;我假设手机的 SD 卡
我正在编写一个文件,该文件是 npycurses module 的子类 npycurses.ActionFormV2 ,我正在覆盖 beforeEditing method ,所以当我整理我的文件时,
关闭。这个问题是opinion-based .它目前不接受答案。 想要改进这个问题? 更新问题,以便 editing this post 可以用事实和引用来回答它. 关闭 8 年前。 Improve
我正在开发一个可在 iPhone、iPad 和其他移动设备上运行的网页。我很好奇是否有关于移动设备 CSS 最佳实践的资源。 我试过搜索,但想出了随机网站 with tidbits of inform
我发现开始编程时最大的挑战之一是掌握文件路径。例如,当您引用目录结构中同一级别的文件夹中的路径时,您将看到: /folder/style.css 或 文件夹/style.css等 当您引用不同的文件时
如果我有如下函数: def foo(): return 1, 2 我通常会这样调用函数: g, f = foo() 但是,如果我从不打算使用返回的第二个值,是否有一种调用函数的方法可以明确说明
我是一名优秀的程序员,十分优秀!