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

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

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

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 <stdio.h>

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 <stdio.h>
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
image.png
// eg4
// caller():
//     long sum = swap_add(&arg1, &arg2);
image.png
image.png
image.png

64位系统, 指针 为 8 Byte

2. 寄存器 中 局部存储

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

#include <stdio.h>
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 的倍数`
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容