深入理解 函数调用: 函数调用栈 / 寄存器传递 / 汇编

caller: 主调函数
callee: 被调函数

1 x86-64 / Inter 处理器: 1 个 CPU16 个 64 位 通用目的 寄存器, 存 整数指针

深入理解 函数调用: 函数调用栈 / 寄存器传递 / 汇编_第1张图片

1.1 运行时栈: 3 类 寄存器

为确保 callee `不会覆盖 caller 稍后要用的 寄存器值`,
x86-64 有一组 `规范:` 何时 use 哪个 寄存器 

(1) callee 保存 寄存器

%bp

可作 帧指针, 存储 `当前栈帧 的 栈底; 
%bp 入栈`, 通常是为了 `保存 前 1 栈帧 的 栈底`**

%bx

%r12-15

(2) caller 保存 寄存器

%ax : 返回值
%di %si %dx %cx %r8 %r9 : 第 1-6 个参数

//前1-4个分别是 
d s d c //连接
i i x x 

(3) 栈指针

%sp

1.2 callee / caller 保存 寄存器

(1) callee 保存 寄存器
`P 作为` main 的 `被调用者`

callee 保存, 保证 其 ( callee~器 ) 值callee 返回 时 与 callee 被调用相同

(2) caller 保存 寄存器

保存 caller~器 是 caller 的责任

<=
`P 作为` Q 的 `调用者`
1) caller 在 caller~器 中 store 旧值
2) caller 调用 callee
3) callee 可随意修改 之
4) 调完 后, caller 还要用 stored 旧值

1.3 如何保证 函数调用 正确进行

答:只需让 

正在运行的过程需要时 ( 什么情况下需要 ? ) 正确保存 ( 如何保存 ? ) callee / caller ~器

(1) 若 calleecallee~器 ( %rbx ), 由 callee 通过 将 callee~器 ( %rbx ) 入/出 ( callee ) 栈保存 callee~器

// main 调 P
1) callee 将 %rbx (旧值) `push 到 callee 栈`  
2) callee 中 mod %rbp 
3) callee 返回前, `pop %rbp (旧值)` from callee 栈

(2) 若 calleecaller~器 ( %rdi )caller 调用 callee 后 还要用 caller~器, 由 caller 通过 将 caller~器 ( %rdi ) 保存到 callee~器 ( %rbx )保存 caller~器

P ( caller ) 调 Q (callee)
`Q 会 改 %rdi` + Q 返回后, `P 还要用 %rdi (旧值)`
=>

1) `P 保存 %rdi (旧值) 到 %rbx`

    => `P 改 %rbx 
    => P 要先保存 %rbx ( 给 P 的 caller 用, 此时 P 又作为 callee ):
        将 %rbx push 到 P 栈帧, P 返回前 pop %rbx`

2) P 调 Q

3) Q 改 %rdi 

4) `P 用 %rbx 恢复 %rdi ( 旧值 ), 再 use %rdi ( 旧值 )`
##1.4 eg
// eg1: 函数调用 底层机制
// 函数调用 过程中 callee / caller 保存寄存器 如何 被 callee / caller 保存?

// long P(long x):
//     return x + Q(0); 

//.c
#include 

long Q(long x)
{
    x = 2;
    return x;
}
long P(long x)
{
    // 2-1 %rdi: 旧值 x
    
    int y;
    
    // 2-2 %rdi 被 Q (作 P 的 callee) 修改: 新值为0
    y = Q(0); 
    
    // 2-3 P 调 Q 之后, P (作 Q 的 caller) 还要用 %rdi 旧值x
    return x + y; 
}

int main()
{
    long x = 10;
    long y = P(x);
    printf("%ld", y);
}

//.s
Q:
    movl    $2, %eax
    ret
P:
    pushq   %rbx       // 1-2 `由 P (作 main 的 callee) 保存 %rbx`: 通过 入/出 P 栈帧
    movq    %rdi, %rbx // 1-1 %rbx 被 P (作 main 的 callee) 修改 / 2-4 `由 P ( 作 Q 的 caller ) 保存 %rdi` ( 旧值 ) 到 %rbx => 1
    movl    $0, %edi
    call    Q
    cltq
    addq    %rbx, %rax
    popq    %rbx
    ret
// eg2: caller 保存寄存器 无需保存 的 case
//.c
long P(long x)
{
    // 1. %rdi: 旧值 x
    
    // 2. %rdi 被改: 新值 x + 1
    return Q(x+1); 
     
    // 3. 之后, P ( caller ) 不再用 %rdi 旧值 x => P ( caller ) 不用保存 %rdi
}

//.s
P:  // P 不用保存 %rdi
    addq    $1, %rdi  
    call    Q
    ret

1.5 为什么 caller~器 要由 caller ( 而不是 callee ) 保存

答: 

eg: 
`P 调 Q + Q 改 %rdi + Q 返回后, P 还要用 %rdi (旧值):`

long P(long x):
     return x + Q(0); 

`若 由 callee 保存`

Q 保存 %rdi 到 %rbx -> until Q 返回 P 时, 才能用 %rbx 恢复 %rdi ( 旧值 )

=> `Q (作为 P 的 callee) 不能 通过 入/出 Q 栈 来 保存 %rbx`
=> Q 返回 P 时, %rbx 的 值被改了

保存了 %rdi, 却把 %rbp 值丢了

2 汇编:试图最大化一段代码的性能

PC: 程序计数器, 存 下一条指令(在内存中)的地址

linux下 由 .c 文件 得到汇编文件 .s:

gcc -Og -S hello.c

2.1 三种操作数

立即数 immediate
寄存器 register
内存引用

2.2 数据传送指令 : 将数据从一个位置复制到另一个位置

1. 指令结构
最后2个字符:分别是 源和目的的大小
倒数第3个字符:z - 零扩展,s-符号扩展

2. MOV 系列指令的 5种情形:src -> dst

//Immediate -> Register : 
movl $0x4050, %eax     // 0x4050 -> %eax

//Immediate -> Memeory  : 
movb $-17, (%rsp)      // -17 -> * %rsp

//Register  -> Register : 
movw %bp, %sp          // %bp -> %sp

//Register  -> Memeory  :
movq %rax, -12(%rbp)   // %rax -> *(%rsp - 12)

//Memory    -> Register : 
movb (%rdi, %rcx), %al // *(%rdi + %rcx) -> %al

(1) dst 必须是 Register/Memory
(2) Memory 不能到 Memory
(3) 1方 非 内存 时,传送的是 Immediate/Register 本身的值
(4) 1方 为 内存, 即 用 offset( ... ) 形式时,这1方交互的是 () 中的 值 和 偏移 形成的 内存地址 上的值

3. 指针的 间接引用 pointer dereferencing**
x = *xp; // `读` sp 所指内存中的值 到 x
*xp = y; // `写` y 的值 到 sp 所指内存

2.3 压入 和 弹出 栈数据

栈指针 %rsp: 保存 栈顶 元素地址

入栈: 栈指针 减8, 值 写 到栈顶

pushq %rbp
<=>
subq $8, %rsp
movq %rbp, (%rsp) // write %rbp on stack

出栈:读 栈顶数据, 栈指针加8

popq %rax
<=>
movq (%rsp), %rax  // read %rax from stack
addq $8, %rsp

2.4 算术

1. 加载有效地址

leaq: 将 有效地址 写到 目的操作数

note: 形式 是 从内存 `读数据` 到寄存器

leaq 7(%rdx, %rdx, 4), %rax

将 %rax 的值 设为 5x + 7, 假定 %rdx 值为 x
2. 二元操作

`第2操作数: 既是源又是目的`
必须是寄存器或内存
当为内存地址时, 
CPU 从内存中读出值, 
执行操作, 
结果写入内存

2.5 跳转

jmp *%rax     // 新地址为 %rax 
jmp *(%rax)   // 新地址为 以 %rax 的值为地址的内存中的值

3 过程/函数调用 的机制

函数 如何 保存现场 并 返回

`过程`: 抽象机制
函数 function, 方法method, 子例程 subroutine, 处理函数 handler

P 调用 Q, Q 执行后 返回到 P:
1)传递控制

1) `进入 Q 时`, PC 设为 `Q 第1条指令的地址`
2) `Q 返回 时`, PC 设为 `Q 的 返回地址`:
    P 中 调用 Q 指令 后面 那条指令的地址

2)传递数据

P 给 Q 提供 参数, Q 向 P 返回1个值

3)分配 和 释放内存

Q 开始时 要为 `loacl 变量 分配空间`,
Q 返回前 必须释放这些空间

3.1 运行时栈

栈帧结构被保存的 register: push 到 栈帧 的 register

C 语言 过程调用机制 的关键:使用 提供的 后进先出内存管理

栈帧: 栈上分配的空间

例: P 调 Q, Q 正在执行
1) `P 及 P 的 调用链 中 过程`, 都暂时被 `挂起`
2) 系统分配  P 的栈帧:

将 栈指针减小/增加 可以 在栈上 分配/释放 空间

1. 何时必须 用栈传递?

3种情形之一:

(1)需保存 被保存的寄存器 : 即 被调用者保存寄存器

(2)存在 loacl variable必须通过 栈 传递

1)寄存器 不够存 所有 local variable
2)local variable 用 取址运算符 &: 
因为 `只能对 内存 取地址`
3)local variable 为 数组或结构:
数组和结构占用的空间是 一段连续的内存

(3) 该过程 又调用新过程, 而 寄存器 不够存 调用 新过程 所用的所有 arg(7-n个arg)

P 调 Q, Q 正在执行.png

2. P 调 Q, Q 的 返回地址 作为 P 的 栈帧 的一部分, 因为它存放 与 P相关的状态

  1. P 调 Q 时的 参数传递:

每个栈帧基于一个函数, 栈帧 随着 函数的生命周期产生、发展和消亡

(1)`寄存器传递:` 最多传 `6个整数值(指针 和 整数)`

(2)`栈传递: 第7-n个参数, 参数 7->n 反顺序压栈 
=> 参数 7 在栈顶`

栈帧结构中用到2个寄存器来定位 当前帧的空间, 
实际上 未必一定需要帧指针:
`%ebp/%esp : 帧/栈`指针, 
    总是指向当前帧的底/顶部

`编译器 根据 汇编指令集` 规则 `小心调整 %ebp %esp 的值`

3.2 转移控制

Q 的返回地址 : P 中 call Q 指令的 下一条指令的地址

1. 如何将 控制 从 P 转到 Q ?

(1) call Q
1) Q 返回地址 压栈
2) 设 PCQ 第1条指令 地址

(2)去执行: 执行 下一条指令 / PC 所指 指令

2. 如何将 控制 从 Q 返回到 P ?

(1) ret
1) 弹栈 出 Q 返回地址
2) 设给 PC

(2)去执行

image.png
image.png

3.3 数据传送

1. P 调用 Q

(1)参数传递 : P 把 
`参数 1-6 复制到 适当的寄存器`; 
`参数 7-n 用 栈传递, 数据大小 都向 指针大小`
    = 8 (64位系统) 的 倍数对齐
(2) call Q: 控制转移到 Q 

2. Q 返回 P: P 可访问 寄存器 %rax 中 返回值

3.4 函数调用时 参数的传递: 寄存器传递/栈传递

1. 栈 上 局部存储

(1) `栈就是一段内存, 用来进行 内存管理``

栈 的 意义: 函数调用 中 保存/恢复 被保存的寄存器 / local variable / 实参argument / 返回地址 等

(2) 不需要 出栈/入栈 就能 读取/写入 栈中任何位置的内存值

, 通过 `栈指针 加 偏移 加 读/写 操作` 即可
// eg3
// long call_proc(): 
//     proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);

//主调函数 .c
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);
}
//.s
call_proc
    // set up arguments to proc: 为调用 proc 作准备
    // alloc 32-byte stack frame, 不算 call_proc 中 被调函数 proc 返回地址 所占空间, 
    // 因 call proc 里包含 proc 返回地址 压栈
    subq    $32, %rsp       
       
    // 1. local variable space in stack
    movq    $1, 24(%rsp)    // store 1 in  &x1
    movl    $2, 20(%rsp)
    movw    $3, 18(%rsp)
    movb    $4, 17(%rsp)

    // 2. argument 7-8 : pass by stack
    leaq    17(%rsp), %rax  // create &x4, %rax 只用作普通寄存器, 作存储用
    movq    %rax, 8(%rsp)   // store &x4 as argunent 8
    movl    $4, (%rsp)      // store 4 as argunent 7

    // 3. argument1-6 : pass by register
    leaq    18(%rsp), %r9   // pass &x3 as argunent 6
    movl    $3, %r8d        // pass 3 as argunent 5
    leaq    20(%rsp), %rcx
    movl    $2, %edx
    leaq    24(%rsp), %rsi
    movl    $1, %edi
        
    // 4. call proc()
    call    proc

    // 5. Retrive changes to memory, %rdx %eax %ecx 这里只用作普通存储器, 作存储用
    movslq  20(%rsp), %rdx  // get x2 and convert to long
    addq    24(%rsp), %rdx  // compute x1+x2
    movswl  18(%rsp), %eax
    movsbq  17(%rsp), %ecx
    subl    %ecx, %eax
    cltq                   //convert to long
    imulq   %rdx, %rax
    addq    $32, %rsp
    ret
图中的数字为 `字节序号`, 
64 位OS, 
`指针大小 = 地址总线 = 64位 = 8 Byte`
image.png
(3) call_proc 汇编 大部分是为 调用 proc 作准备:

1) 栈上为 局部变量 x1 - x4 建立栈帧, 即 分配存储空间
2) `leaq 指令 生成到 到这些变量 内存的 指针`
3) 为 参数8-7 建立栈帧
4) 参数 1-6 加载至 寄存器

call_proc:
    执行 call proc 指令: 
        proc 返回地址 入栈, 
        subq $8, %rsp // 在栈上 分配空间

proc: 
    执行 ret 指令:
        proc 返回地址 出栈, 
        addq $8, %rsp // 在栈上 释放空间

note: 书中画成下图, 应该不对
image.png
//note: 1个函数也可以直接在 linux 下 进行汇编,得到.s文件
//被调函数 .c
#include 
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;
}

//.s
proc: // 函数 proc 没有栈帧
    movq    16(%rsp), %rax
    addq    %rdi, (%rsi)
    addl    %edx, (%rcx)
    addw    %r8w, (%r9)
    movl    8(%rsp), %edx
    addb    %dl, (%rax)
    ret
image.png
深入理解 函数调用: 函数调用栈 / 寄存器传递 / 汇编_第2张图片
image.png
// eg4
// caller():
//     long sum = swap_add(&arg1, &arg2);
深入理解 函数调用: 函数调用栈 / 寄存器传递 / 汇编_第3张图片
image.png
image.png
image.png

64位系统, 指针 为 8 Byte

2. 寄存器 中 局部存储

//eg5
// long P(long x, long y):
//     return Q(x) + Q(y);    

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

int main()
{
    long x = 10;
    long y = 20;
    long z = P(x, y);
    printf("%ld", z);
}
Q: // note:Q 无栈帧, Q 参数个数<7, 所有 参数都通过 寄存器传递了
    leaq    (%rdi,%rdi), %rax
    ret
P:
// x in %rdi
// y in %rsi
    pushq   %rbp       // save %rbp
    pushq   %rbx       // save %rbx
    movq    %rdi, %rbp // save x(%rdi) to %rbp
    movq    %rsi, %rdi // move y(%rsi) to first argument %rdi
    call    Q          // call Q(y)
    movq    %rax, %rbx // Save Q(y)(saved in %rax) result to %rbx
    movq    %rbp, %rdi // move x(saved in %rbp) to first argument %rdi
    call    Q          // call Q(x)
    addq    %rbx, %rax // add Q(y)(saved in %rbx) to Q(x) (saved in %rax)
    popq    %rbx       // restore %rbx
    popq    %rbp       // restore %rbp
    ret
这里, 只着重分析 P的 汇编 和 栈帧变化:

(1) P的参数
x : %rdi
y : %rsi

(2) P 中 local 变量

P正在运行 行为和栈帧变化:

1)`P 保存 调~器 %rdi / %rax 到 被~器 %rbp / %rbx`

1> %rdi: 
先存旧值x 
-> P调Q(y)时, 被P修改为新值y 
-> P调Q(x)时, P又要用其旧值x

note: `P不修改 %rsi => P 不用保存 %rsi`

2> %rax: 
P 调 Q(y)时,  先存 %rax 旧值 Q(y) 
-> P 调 Q(x) 时, %rax 被 P 修改为新值Q(x) 
-> 返回 u+v 时, P又要用其旧值 u=Q(y)

2)`P 保存 被~器 %rbp  %rbx 到 P 的 栈帧`
P 保存 %rdi / %rax 到 %rbp / %rbx,
即 P 会修改 被~器 %rbp  %rbx
=> P 要先保存 %rbp  %rbx

(3) P的栈帧变化:
image.png

4. 数据对齐

简化 CPU 与 内存系统接口 硬件设计

1. Intel x86-64 建议的 `对齐原则:`

(1) K 字节 基本对象 的 地址 必须是 K 的倍数

每种类型的对象 都满足 自己的对齐限制, 就能 保证对齐

image.png

(2) struct 类型的 指针:

`成员 j: 4 字节对齐 => 其 地址是 4 的倍数`

你可能感兴趣的:(深入理解 函数调用: 函数调用栈 / 寄存器传递 / 汇编)