函数调用的汇编原理

一. 函数调用基本概念

函数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语言中的类型转换

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值