.net core底层入门学习笔记(四-汇编函数调用)

.net core底层入门学习笔记(四)

本篇主要记录,汇编-函数调用相关知识,重点需要结合之前的IL代码中的CALL命令



前言

函数的调用,主要注意的是调用规范,即函数与函数(也就是指令之间跳转)如何交换数据,以及如何回到被调用的函数。


一、栈结构

栈结构支持元素,先进后出,由于每个函数都有自己的数据,而调用函数也是先进后出的逻辑。所以用栈结构支持函数数据就很适合。

  • X86中每个线程有一个自己的栈结构,栈结构从大地址到小地址,最大地址称为栈底,最小地址为栈边界。
  • 栈内部每个函数会占用一块数据内容,每一块函数数据占用的栈结构,称为栈帧。
  • 记录最后一个添加到栈边界元素所在地址的地址,称为栈顶。如果栈顶小于栈边界时,则发生栈溢出。
  • 栈顶的值由寄存器esp保存,称为栈寄存器,进入某个函数的栈帧结构时的esp地址由ebp寄存器保存,称为帧寄存器。向栈添加或删除元素时,会改变esp寄存器内保存的地址值。用push,pop指令添加删除栈元素,也可以直接修改esp寄存器的值达到相同目的。

这里要重点区分栈与指令,第一次看本书时,理解错误导致无法完全了解原理
栈与指令都存在内存中,属于不同内存区域;指令会操作栈的数据,栈的存在主要用于函数之间的数据交换;函数调用链的跟踪;函数功能实现;函数跳转等功能
不同架构可能会通过不同的函数调用约定,也就是对内存不同的操作,实现函数调用与跟踪

二、函数调用

1.X86函数调用规范

调用函数流程:假设有两个函数,一个add,一个example,在example中调用add函数:

int add(intx)
{
	int y = x + 1;
	return y;
}


void example()
{
	int y = add(0X7a);
	int z = 1;
}

函数调用规范,

(1).进入任何函数时的指令流程如下:

  1. push ebp,把当前ebp的值备份到栈结构中,用于恢复。(此时esp地址被修改指向增大的地址)

  2. move ebp,esp,把当前esp的值复制到ebp中,这样ebp就指向了当前函数的入口处了。

  3. sub esp,4,分配这个函数的本地变量(此时esp指向了新的地址)

  4. move exc,dword ptr[ebp+8],将此时ebp+8地址指向的值复制到exc(ebp+8的地址,是上个函数调用时,放到栈里面的参数),这样就完成了一次函数之间的参数交流。

  5. mov eax,dword ptr[epb-4]经过当前函数一系列指令的计算,对栈进行各种操作,最终将返回值从本地变量(通过ebp-4确认栈地址)值赋值到eax中,这时eax的值就是函数的返回值供调用函数使用。

  6. move esp,ebp 退出函数时,将ebp值赋值给esp,这样esp就回到第二步中的状态

  7. pop ebp,从栈中取出值赋值给ebp,这时候esp在第二步状态回到第一步指向的地址,而取出的值就是第一步备份的ebp的值,此时ebp,esp都回到进入函数时的地址值了。

  8. ret指令的执行

(2).调用任何函数时的指令流程如下:

首先会进入当前函数,与上面相同,执行到要调用函数时

  1. push 7a,将参数7a放入栈中,(esp会变化)
  2. call add:此指令重点注意,分为两步:1.将本来应该执行(此函数中就是 int z = 1这条指令)下一条指令地址放入栈中,称为返回地址,用于调用函数执行完之后跳转回本函数指令;2.jmp 目标函数地址,直接跳转到add函数指令所在位置,即进入步骤上个大步骤,进入函数的流程中去。
  3. 当执行到被调用函数add的ret指令时,读取上面存放的返回地址(esp回到call的位置了),然后跳转回本函数的下一条指令地址,继续执行example函数的指令

通过上述两个部分,一个调用流程,一个进入函数流程,通过栈的方式,能够实现完整的函数调用流程。本规范是X86的cdecl规范,还有其他函数调用规范,但是使用的方式都差不多,可以自行查阅资料了解。

2.调用链跟踪

通过对当前线程的寄存器状态,元数据获取到所有函数入口点和机器码大小,可以实现函数调用链跟踪。
具体来说:

  1. 查看eip,保存的下一条执行指令的地址,可以获取到当前指令内存地址,然后查阅元数据可知道是哪个函数正在执行
  2. 通过当前ebp寄存器地址+4,取值(32位),获得当前函数执行完毕后会调用的返回地址,再次查阅元数据,就可以知道当前函数的上一个调用函数信息
  3. 通过当前ebp寄存器地址,取值(32位),这是备份的进入此函数时ebp的值,即进入上个函数ebp的值。然后又通过步骤2,获取当前函数,调用者的,调用者的函数信息
  4. 其他以此类推。

3.省略帧寄存器

进入函数与退出函数,都会使用帧寄存器,来还原esp的值,其实可以使用sub与add命令直接修改esp指向的地址,这样就能省略使用帧寄存器了。
但是X86标准调用并没有使用这种方式,因为这种方式可以使用通用的调用链跟踪方法。不过在X86-64调用规范不要求使用帧寄存器,那么通用调用链跟踪方法就无法使用了,可以使用其他方法,比如在元数据中记录函数在什么位置栈寄存器会发生什么变化,进而推算返回地址的具体地址位置。
enter,leave指令可以简写函数进入和离开时备份恢复栈寄存器的指令,但是enter具有性能问题,一般不会使用,但是leave指令不会有问题,所以一般都会使用这个指令代替还原esp寄存器的逻辑,用以缩短机器码长度(节省内存)。

4.其他调用规范

在不平台有不同的调用规范,且一个平台自身也会又不同的调用规范,所以不同调用规范之间不能直接互相调用的(但可以使用一些特殊方法间接互相调用)。调用规范主要定义如何在不同函数调用时,互相传递数据,定义了参数在栈中内存的前后顺序,调用哪些指令寄存器会发生哪些变化。有些规范定义参数不是全部都进入栈的,而是部分通过寄存器交互,部分进入栈交互。还会定义函数返回值,存在哪个寄存器中。

总结

本节主要讲述,在汇编中,函数之间通过规范,如何进行数据的交互,参数的调用,如何从一个被调用函数中回到调用函数的下一条指令,以及如何获得被调用函数的返回值。
函数的调用,都是通过灵活使用寄存器,指令地址,栈内存实现的,了解这些方法,有助于了解.net中IL代码是如何进行封装使用的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值