函数调用的汇编原理

一. 函数调用基本概念

函数f调用函数g, 则称f为caller,g为callee

Pasted image 20231115133428
栈帧: 程序进行时系统自动分配的栈区域

%rsp 是一个指针指向栈顶部(用来确认栈的位置),并会随程序运行动态变化

二. 函数调用需解决的5大问题

  1. 怎么调用callee函数
  2. 怎么返回caller函数
  3. 怎么传参以及怎么返回值
  4. 寄存器共用的问题怎么解决
  5. 局部变量存在哪里

1. 怎么调用callee函数

在汇编代码中,执行函数调用采用的是call指令
和jump类似,它也有直接和间接之分

  • call label (direct)
  • call *operand (indirect)
    执行call指令时,机器执行了以下操作:
  1. 将返回的地址存在了栈中
  2. 跳到了callee函数的路口
    可以这么理解:call = push + jmp
    push retaddr
    jmp callee

举个栗子:
Pasted image 20231115142201
执行call前后%rip和%rsp的变化:
Pasted image 20231115140626

push指令含义:

  1. %rsp-8(地址占8个字节);
  2. M[%rsp] = 0x400568(return adress)

2. 怎么返回caller函数

ret指令 (return缩写?)
机器执行的操作:

  1. 将返回地址从栈中弹出
  2. Jump到该return address(回到caller中)
    ret = pop + jmp
    pop retaddr
    jmp retaddr

还是上面的例子,%rip和%rsp的变化:
Pasted image 20231115142239
栈帧的结构:
Pasted image 20231115142614
%rsp仅指向栈帧的最顶部!
返回前,Callee Frame内的各个数据出栈(%rsp上移),直到readdr
然后执行ret,pop readdr

3. 怎么传参以及怎么返回值

使用寄存器以及栈存储变量

系统使用了特定的寄存器用来传参
其中整数寄存器有6个, 分别是 %rdi, %rsi, %rdx, %rcx, %r8, %r9
(对应的字节数更少的表示同理,如%edi, %dl等)

特定的寄存器存返回值: %rax

? 如果参数个数多于6个呢
Pasted image 20231115143453

  • 将多出的参数保存在caller的栈帧中
  • 参数就保存在retaddr的上面
  • 参数存放的顺序是从第N个到第7个(顺序很重要!
    即第N个参数先入栈,内存位置最大
    第7个参数最后入栈,内存位置最小
  • 所有的数据类型(大小)都被扩充为8的倍数
    这些参数如何被callee访问: %rsp + 偏移量

调用前汇编代码呈现:

push argument N

push argument 7
movq argument 6 %r9

movq argument 1 %rdi
call callee

以下是一个调用实例:

void proc(long   a1,  long  *a1p,
         int      a2,  int     *a2p,
         short  a3,  short  *a3p,
         char   a4,  char   *a4p)
 {
	  *a1p += a1 ;
	  *a2p += a2 ;
	  *a3p += a3 ;
	  *a4p += a4 ;
 }

Pasted image 20231115153459

4. 寄存器共用的问题怎么解决

由于寄存器被所有进程共用,要求:

  1. 只有1个进程处在active状态
    意思是在该进程运行中通过中可以随意修改寄存器的值,而其它进程无法修改
    那其它进程中储存在寄存器中的值被破坏了怎么办?
  2. 将共用的寄存器内容彼此隔离
    即要保存寄存器中的值到栈中,具体分为:
    • caller-save register

    即寄存器值由caller保存,存在Caller Frame中

    • callee-save register

    即寄存器值由callee保存,存在Callee Frame中

  3. 不管是哪种save,只要考虑被进程使用的寄存器,不使用的不用save
  • Caller-save registers

    • %rax, %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11
    • Saved by caller
    • Callee can use these registers freely
    • The contents in these registers may be changed after return
    • Caller must restore them if it tries to use them after calling

Caller-save registers一般用于函数的传参,以及用于返回值(%rax),即希望callee修改这些寄存器的情况
如果希望保留原寄存器中的值。则要进栈
Pasted image 20231116130913

  • Callee-save registers
    • –%rbx, %rbp, %r12-15
    • Saved by callee
    • Caller can use these registers freely
    • Callee must save them before using
    • Callee must restore them before return

callee-save registers一般用于确保caller中寄存器的值不会被修改,使得callee可以放心地使用寄存器进行操作

下面举个栗子:

 long P(long x, long y)
 {
   long u = Q(y);
   long v = Q(x);
   return u + v;
 }

P的汇编代码:

LineInstructionDescription
1P:Label definition for P
2pushq %rbpSave %rbp (callee-save register)
3pushq %rbxSave %rbx
4subq $8, %rspAlign stack frame
5movq %rdi, %rbpSave x (exists in callee-save register, safe)
6movq %rsi, %rdiMove y to the first argument
7call QCall Q(y)
8movq %rax, %rbxSave the result
9movq %rbp, %rdiMove x to the first argument
10call QCall Q(x)
11addq %rbx, %raxAdd saved Q(y) to Q(x)
12addq $8, %rspDeallocate the last part of the stack
13popq %rbxRestore %rbx
14popq %rbpRestore %rbp
15retReturn

5. 局部变量存在哪里

存在栈上(即答)
不存在寄存器中的理由:

  1. 数量不够
  2. 变量是数组(连续存储) 以及 结构体(一组连续数据)
  3. 需要取地址(比如&a)

存放的位置: 就存在calle-save rigisters 的下面
释放:add %rsp
访问: %rsp + 偏移
Pasted image 20231116133037
举一个变量存放的栗子:

long call_proc()
{
  long x1 = 1; int x2 = 2;
  short x3 = 3; char x4 = 4;
  proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
  return (x1+x2)*(x3-x4);
}

Pasted image 20231116133351

leaq   17(%rsp), %rax      Create &x4
movq   %rax, 8(%rsp)       Store &x4 as argument 8
movl   $4, (%rsp)          Store 4 as argument 7
leaq   18(%rsp), %r9       Pass &x3 as argument 6
movl   $3, %r8d            Pass 3 as argument 5
leaq   20(%rsp), %rcx      Pass &x2 as argument 4
movl   $2, %edx            Pass 2 as argument 3
leaq   24(%rsp), %rsi      Pass &x1 as argument 2
movl   $1, %edi            Pass 1 as argument 1

总结

本文介绍了函数调用是怎么在机器上实现的,并给出了相应的汇编代码.

更多文章:
数据在内存中的对齐问题
计算机编译程序的原理
汇编语句详解(持续更新)
关于位运算必须记住的事
C语言中的类型转换

  • 30
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
汇编语言中更改函数调用约定通常涉及到规定参数、返回值和返回地址等放置的位置,以及如何管理栈的操作。以下是一些关键点: 1. **了解调用约定**:调用约定是一组规则,它们定义了在函数调用发生时,调用者(caller)和被调用者(callee)应如何协作。这包括参数的传递方式、返回值的处理以及栈的管理等方面。 2. **参数传递**:在不同的调用约定中,参数可以通过寄存器或栈来传递。例如,在System V AMD64调用约定中,前几个参数通常会通过特定的寄存器传递,而超出的部分则通过栈传递。 3. **栈的使用**:栈在函数调用中扮演着重要角色,它遵循先进后出的原则。在函数调用过程中,栈被用来存储局部变量、参数以及返回地址等信息。使用push与pop指令对栈空间执行数据的压入和弹出操作。 4. **返回值处理**:调用约定还规定了函数返回值的处理方式。有些调用约定要求返回值存放在特定的寄存器中,而有些则通过栈来传递返回值。 5. **调用者和被调用者的协作**:在C/C++中,CDECL是一种默认的调用约定,它规定函数调用者负责将参数压入栈中,并在调用后清理栈。而被调用函数通过栈顶指针ESP来访问这些参数。 6. **特定平台的约定**:不同的平台和架构可能有不同的调用约定。例如,在32位系统上,存在多种调用约定,如cdecl、stdcall、fastcall等。而在64位体系架构下,现代编译器通常会遵循System V AMD64调用约定。 要更改函数调用约定,你需要根据目标平台和编译器的规定来调整汇编代码。这可能涉及到修改函数的prologue和epilogue代码,以及调整参数传递和返回值处理的方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值