剖析函数 — 数据怎么在函数之间流动:全局、参数、栈与三种生存期
剖析函数 — 数据怎么在函数之间流动
这一章讲什么: 把一个大问题拆成若干函数之后,真正的难题出现了——数据怎么在函数之间流动? 本章把答案收成四条路(全局、按值、按指针/引用、返回值),每条路都走到汇编层看清 「值住在哪、地址怎么递、谁在什么时候改」。走完后你会拿到一份「栈帧协议总表」 (书里 17 条规则),那是理解任何编译器输出的万能钥匙。 主线例子:
addNine(1, 2, …, 9)——9 个参数的函数,前 6 个挤寄存器, 后 3 个住栈,我们跟着它们从 main 的局部变量一直走到 addNine 的加法器。
1. 顶层全景:函数间的数据接口
为什么拆函数要付这笔账
「分而治之」的好处书里列了三条:小问题容易解决、解法可复用、多人可并行1。 但拆开就要接回去:每个函数是个黑盒,盒与盒之间只靠一个「数据接口」说话, 接口必须以明确的方式指定——否则各半套解法拼不出一个正确答案1。
先分清两个容易混的「输入/输出」:本章讲的是函数之间的数据传递
(比如 main 把一个整数递给 addTwo),不是用户与程序之间的(键盘进、屏幕出)。
书里特意用 scanf("%x", &anInt) 把两者摆在一起:格式串地址是「函数的输入」,
键盘敲的数是「用户的输入」——前者是我们本章的主角2。
函数与其他部分的数据交互,只有四种
书里给了一张很有用的分类,任何函数与外界的来往都能归进去2:
| 交互 | 含义 | 例子 |
|---|---|---|
| 全局 | 任何函数可直接访问 | 全局变量 |
| 输入 | 数据进来,原件不动 | 按值传参 |
| 输出 | 函数产出新数据 | 返回值、写进传入的地址 |
| 更新 | 基于旧值改原件 | 通过地址修改调用方的变量 |
注意「输入」这行的潜台词:按值传递的副本机制,就是被调用方碰不到原件的保证。 后面所有传参方式的选择,本质都是在这四格之间做取舍2。
栈帧全景(书里的图 14-4,本章的地形图)
高地址
┌──────────────────────┐
│ 调用方压入的栈参数 │ ← 第 7 个起(用 rbp 的正偏移访问)
├──────────────────────┤
│ 返回地址 │ ← call 压入(唯一必定存在的格子)
├──────────────────────┤
│ 调用方的 rbp │ ← 被调方序言压入
├──────────────────────┤ ← rbp 指着这里
│ 寄存器保存区 │ ← rbx、r12–r15 等(如果用得到)
│ 局部变量 │ ← rbp 的负偏移访问
│ (红区:rsp 下方 128 字节)│ ← 叶函数的临时仓库
└──────────────────────┘ ← rsp 指着这里
低地址
图说:从上到下,先进栈的是参数、再是返回地址,被调方接管后往下盖自己的房子。
全书说「栈帧」,说的就是这张图;每一块在后面的章节里都会填进真东西。书里强调:栈帧不一定包含所有这些格子——参数不超 6 个就没有最上面那块, 有的函数没有局部变量、不保存寄存器;唯一必定存在的,是返回地址3。
2. 变量名的地盘:C 的四种作用域
在谈传递之前,先立「名字在多大范围内可见」的规则。作用域(scope)就是名称在代码中 可见的区域,C 一共有四种4:
| 作用域 | 范围 | 例 |
|---|---|---|
| 文件作用域 | 定义点到文件结束 | 全局变量 |
| 函数作用域 | 函数内(含参数列表) | 局部变量 |
| 块作用域 | 一对花括号 {} 内 | for 循环里的 i |
| 函数原型作用域 | 仅限原型本身 | double sin(double x); 里的 x |
两个配套概念:声明给作用域引入名字和类型;定义还会分配内存。
一个变量只能定义一次,但可以在多个作用域里声明4。函数原型那个作用域
存在的意义很实际:它允许 sin 和 cos 的原型用同一个参数名 x 而互不打架4。
判断(我们的,不是书里的): 作用域是「编译期的名字可见性」,生存期是 「运行期的内存存亡」——两词常被混用,但本书 14.6 节的三类变量对比 (自动/静态/全局)恰恰是故意把它们拆开看:静态局部变量正是「作用域是函数的、 生存期是程序的」这个组合的产物。分清这两个轴,后面所有变量种类的归类都不背。 如果错,会错在: 有的语言(以及 C 的某些用法,如 static 文件作用域) 把 static 一词同时用在两个轴上,单靠术语推断会误判——始终以「这个组合在 本书里怎么实现」为准。
3. 全局变量:住 .data / .bss,经 rip 访问
C 层:定义一次,extern 声明别处用
在所有函数之外定义的变量是全局变量,从定义点到文件尾可见。
别的文件要用它,就用 extern int x, y, z; 声明——extern 只把名字和类型
引进来,不分配内存(内存在定义它的那个文件里)5。
汇编层:三种 assembler 指令,对应三种「初始化状态」
清单 14-2 里 gcc 为三个全局变量(x、y 初始化为 0,z 未初始化)生成了三种不同的定义方式, 这是本章的一个高频考点6:
| 变量 | 汇编指令 | 住在哪 | 为什么 |
|---|---|---|---|
| x、y(=0) | .bss 节 + .zero 4 | bss 段 | 未显式初始化;加载时由系统清零 |
| z(未初始化) | .comm z,4,4 | bss 段 | 同上,另一种写法(带全局作用域) |
| z(=56,清单 14-19) | .data 节 + .long 56 | data 段 | 非 0 初值必须存进可执行文件 |
bss 与 data 的本质区别一句话:bss 不占文件空间——「初始化为 0」这件事由操作系统
在加载时做,文件里只需记「要 4 个字节」;非 0 初值则必须把那个值(56)写进文件,
所以住 .data,用 .long(或 .int)带上值67。
访问:依然是 rip 相对
main 里访问全局变量用的是 mov ecx, DWORD PTR z[rip]——和访问字符串完全同款
的 rip 相对寻址。这之所以可行,是因为加载位置无关可执行文件时,装载器会把 .data 和
.bss 紧挨着 .text 放,偏移量恒定6。addTwo 函数里也一样:
z = x + y 翻译成读两个 rip 相对地址、add、写回第三个,全程没有栈什么事8。
作者的忠告
全局变量在小程序里省事,大程序里是负担:「你需要准确地跟踪程序中的每个函数 对全局变量的处理」。只传需要的变量给各函数,管理起来轻松得多5—— 这就是 14.4 节按值/按指针传递的动机。
头文件:函数原方的「报关单」
清单 14-3 顺带演示了 C 工程的基本组织单位——头文件,机制值得看懂, 因为本书所有多文件程序都这么搭9:
#ifndef ADDTWOGLOBAL_H # 如果没定义过这个「哨兵宏」……
#define ADDTWOGLOBAL_H # ……就定义它,并放行下面的内容
void addTwo(void); # 函数原型:函数名 + 参数与返回值的类型
#endif三件事一句话说完:任何调用 addTwo 的文件都要先「看到」它的原型,
所以 #include "addTwoGlobal.h";宏哨兵 #ifndef/#define/#endif 防止同一个
文件被重复包含时原型写两遍;用大写头文件名当宏名,是为了不跟别人撞车9。
原型只是「报关单」——声明函数长什么样;真正的函数体(定义)只能出现一次,
在 .c 文件里。
主线例子的 C 侧走读:addTwo 到底怎么被调用的
把清单 14-6/14-8 连起来走一遍,接口四要素就齐了10:
- main 里
int x = 0, y = 0, z;三个局部变量,x、y 初值为 0,z 未初始化; - 两个
scanf把用户输入写进 x、y——传的是&x、&y,按指针, 因为 scanf 是「更新」型交互:它要改原件; addTwo(x, y, &z):x、y 按值(输入型),&z按指针(输出型)—— 同一个调用里三种交互方式用全了两种半;- addTwo 内部:
temp = a + b算出和,*c = temp顺着地址写回 main 的 z。 函数返回后,main 的 z 里躺着和——全程没有返回值的影子, 输出走的是「递地址」这条路。
4. 显式传参:按值、按指针、按引用
三种方式,机器层只有两种
书里把接口方式分细11:
- 按值传递(pass by value):传副本。被调方改破天,原件不动;
- 按指针传递(pass by pointer):传地址。C 和 C++ 都有,指针本身还能再指别处;
- 按引用传递(pass by reference):传地址。只有 C++ 有,别名绑死、不能改指别处。
重点在第三段:按指针和按引用在汇编层一模一样——都是一个地址经寄存器递进去;
区别是高级语言强加的纪律,机器不管11。所以读汇编时,
你区分不了「C 的 int *c」和「C++ 的 int &c」。
主线例子第一站:addTwo(x, y, &z) 的接口设计
清单 14-6 的程序把三个变量改成 main 的局部变量,然后:
addTwo(x, y, &z); // x、y 是输入(副本即可),&z 是输出(需要地址)对应的函数签名 void addTwo(int a, int b, int *c)——前两个按值,第三个按指针:
函数内部 temp = a + b; *c = temp; 把结果写回调用方指定的地址10。
汇编层:参数的「寄存器 → 栈」搬家与红区
清单 14-10(addTwo 的汇编)揭示了一个本章最重要的新概念12:
addTwo:
push rbp
mov rbp, rsp
mov DWORD PTR -20[rbp], edi ## a 存进栈
mov DWORD PTR -24[rbp], esi ## b 存进栈
mov QWORD PTR -32[rbp], rdx ## c 的地址存进栈
mov edx, DWORD PTR -20[rbp]
mov eax, DWORD PTR -24[rbp]
add eax, edx ## a + b
mov DWORD PTR -4[rbp], eax ## temp
mov rax, QWORD PTR -32[rbp] ## 取回 c 的地址
mov edx, DWORD PTR -4[rbp] ## 取回和
mov DWORD PTR [rax], edx ## *c = 和
nop
pop rbp
ret怪事一桩:这个函数没有 sub rsp 开栈空间,却用了 −4 到 −32 一大片栈!
答案在 System V ABI 的一个条款里:rsp 以下(地址比它低)的 128 字节叫红区(red zone),
操作系统承诺不使用,函数可以拿来当临时仓库——尤其是不调用别的函数的叶函数
(leaf function),存局部变量连栈指针都不用动12。
红区与栈帧的分工(书里点明):红区是相对 rsp 定义的;而变量和保存的参数 是相对 rbp 访问的——编译器遵守「局部变量区与参数保存区分开」的习惯12。
两个附带观察:
- 寄存器参数在函数开头被存进栈,之后全程从栈读写——因为后面要反复用这些值, 寄存器得腾出来干活(第 11 章表 11-2 说 eax 等不需要保存,但「要反复用的值」 还是落地省心)。被调函数里要再 call 别人时,这一存更是保命(规则 9);
- 那个
nop(无操作,占一字节)是编译器插的,书里说是「微调硬件实现细节以提高效率」, 不影响逻辑13。读汇编时看到 nop,不用慌,它不是你的代码。
5. 第 7 个参数起:走栈
协议:从右到左压栈,被调方直接用,不弹出
超过 6 个参数时,前 6 个照旧坐寄存器(rdi、rsi、rdx、rcx、r8、r9), 其余按「从右到左」的顺序压栈。关键细节:这些参数是在 call 之前压入的, call 又把返回地址压在了它们上面——所以被调函数直接从栈里读参数,绝不弹出; 清理工作(call 之后把栈恢复原状)由调用方负责14。
主线例子第二站:addNine(1..9) 的调用侧
清单 14-12 的 main 要调 addNine(a, b, c, d, e, f, g, h, i)15:
mov r9d, DWORD PTR -20[rbp] ## f → 第 6 参数
mov r8d, DWORD PTR -24[rbp] ## e → 第 5 参数
mov ecx, DWORD PTR -28[rbp] ## d → 第 4 参数
mov edx, DWORD PTR -32[rbp] ## c → 第 3 参数
mov esi, DWORD PTR -36[rbp] ## b → 第 2 参数
mov eax, DWORD PTR -40[rbp] ## a 先暂存 eax
sub rsp, 8 ## 对齐:3×8=24 字节,会破坏 16 字节对齐
mov edi, DWORD PTR -8[rbp] ## i
push rdi ## 第 9 参数入栈(最右先入)
mov edi, DWORD PTR -12[rbp]
push rdi ## 第 8 参数
mov edi, DWORD PTR -16[rbp]
push rdi ## 第 7 参数
mov edi, eax ## a → 第 1 参数
call addNine@PLT
add rsp, 32 ## 清场:3 个参数(24) + 对齐的 8三个要点,全是易错点:
- 从右到左:i 最先进栈、g 最后,所以栈里从上往下是 g、h、i——与被调方 「第 7、8、9 参数」的阅读顺序一致14;
- 对齐:3 个 push = 24 字节,不是 16 的倍数,push 前先
sub rsp, 8垫一步, call 之后add rsp, 32一次清掉(24 + 8)15——和第 13 章手写版那个坑同一族; - 宽度:int 是 32 位,但 push/pop 永远动 64 位。压栈前用
mov edi, …装入 32 位,高 32 位自动清零,push rdi 就把干净的 64 位存进去了15。
主线例子第三站:addNine 内部:正偏移量读参数
call 之后、序言之后,栈帧定型(图 14-3)16:
高地址
│ i(32[rbp])│ ← 第 9 参数
│ h(24[rbp])│ ← 第 8 参数
│ g(16[rbp])│ ← 第 7 参数
│ 返回地址 │
│ 调用方 rbp│ ← rbp
│ 保存的 a–f(红区/负偏移)│
│ sum(-4[rbp])│
└ rsp清单 14-15 里,前 6 个参数从寄存器搬进红区(−20 到 −40),栈上三个直接用
正偏移读:mov eax, DWORD PTR 16[rbp](g)、24[rbp](h)、32[rbp](i)16。
「正偏移 = 调用方的东西,负偏移 = 自己的东西」——这是全章最该刻进手心的坐标系。 书里甚至给了作者年轻时的教训:设计函数时画一张栈帧图, 标出每个参数的确切位置,能避免大量错误17。
比 push 更快的活法:一次 sub,直接存
清单 14-16/14-17 是手写版。push 是两步(减 rsp、存值),连压三次就是三次减三次存;
手写版的优化是一次 add rsp, -32 腾出整块,用 .equ 命名的偏移直接存入18:
.equ seventh,0 / eighth,8 / ninth,16 / argSize,-32
……
add rsp, argSize # 一次腾出 32 字节
mov eax, i[rbp]
mov ninth[rsp], rax # 9 → [rsp+16]
mov eax, h[rbp]
mov eighth[rsp], rax # 8 → [rsp+8]
mov eax, g[rbp]
mov seventh[rsp], rax # 7 → [rsp+0]
……6 个寄存器照旧……
call addNine
sub rsp, argSize # 等价于 add rsp, 32,清场被调侧(addNine 手写版)同样用 .equ:seven=16、eight=24、nine=32,
add edi, seven[rbp] 一句搞定19。给偏移量起名的习惯值得照搬——
书里反复用它把「魔法数字」变成可读文档。
栈帧协议总表:17 条规则
本章的高潮是书里的 17 条规则,分四段(调用方 / 被调方序言 / 函数体内 / 返回), 照单接住并按语义归拢3:
调用方(call 之前): ① 假设 rax、rcx、rdx、rsi、rdi、r8–r11 会被被调方改掉(不指望它们活过 call); ② 前 6 个参数从左到右进 rdi、rsi、rdx、rcx、r8、r9; ③ 其余参数从右到左压栈;④ call。
被调方(序言):
⑤ push rbp;⑥ mov rbp, rsp;⑦ sub rsp, 16 的倍数(局部变量 + 寄存器保存区);
⑧ 动了 rbx/rbp/r12–r15 就必须存、返回前恢复;⑨ 函数里还要 call 别人时,
把寄存器参数存进栈(否则一 call 就没)。
函数体内: ⑩ 只用 rsp 当「栈底警戒线」,别拿它寻址变量;⑪ 寄存器参数(存栈后)和局部变量 走 rbp 负偏移;⑫ 栈上传来的参数走 rbp 正偏移。
返回:
⑬ 返回值放 eax;⑭ 恢复保存过的寄存器;⑮ mov rsp, rbp 撤局部区;
⑯ pop rbp;⑰ ret。
这 17 条就是第 11 章「协议」二字的完整内容——任何符合 System V ABI 的编译器输出, 都不过是这张表的机械展开。
一个编程接口的预告:writeStr 与 readLn
本章的「动手实践」布置了一个大练习:用汇编写 writeStr 和 readLn 两个函数——
前者包装 write 向屏幕输出一个字符串,后者从键盘读一行存成 C 风格字符串,
还要处理「输入超出缓冲区长就丢弃多余部分」的边界20。
这不是可有可无的习题:从第 16 章起,书中程序普遍改用这对函数做输入输出,
本章的练习就是后面所有章节的「标准 IO 库」。 specs 全在练习描述里:
readLn 返回字符数(不含 NUL),超长的部分读进来但丢掉。
6. 静态局部变量:藏在 .data 里的「函数私有全局」
动机:既要跨调用保留值,又要信息隐藏
自动局部变量的短板:函数一返回就销毁,值存不住。想存「函数被调用了几次」这类状态, 用全局变量能存住,但全局变量谁都能碰,信息隐藏没了。静态局部变量正是这个两难的标准解: 作用域仍是本函数(外面看不见),生存期却是整个程序(值跨调用保留)21。
主线例子的总复盘:varLife 两次调用的输出追数
清单 14-18 的运行结果可以用三个变量各自的轨迹完全预测出来(这些数都是书里的常量, 不是编的)22:
main 里: x = 12, y = 34, z = 56
第 1 次 addConst:
自动 x:每次新建,先 = 78, +1000 → 打印 1078,函数返回即销毁
静态 y:首次 = 90, +1000 → 打印 1090,留在 .data 里
全局 z: +1000 → 打印 1056
第 2 次 addConst:
自动 x:又是一个全新的 78,+1000 → 打印 1078(和上次一模一样)
静态 y:接着上次的 1090, +1000 → 打印 2090(累加生效!)
全局 z:接着上次的 1056, +1000 → 打印 2056
回到 main:x 还是 12,y 还是 34——函数里的同名变量跟它们毫无关系;z 已是 2056。这张轨迹表比任何定义都更能说清三类变量的差别:自动变量是每次开门重贴的便签, 静态变量是抽屉里的记账本,全局变量是全屋共用的白板2223。
| 变量 | 定义处 | 住在哪 | 初值时机 | 结果 |
|---|---|---|---|---|
| main 的 x、y | 函数内,自动 | 栈帧 | 每次进函数 | 各自独立 |
| addConst 的 x | 函数内,自动 | 栈帧 | 每次调用重新 = 78 | 每次加 1000 都从头 |
| addConst 的 y | 函数内,static | .data | 仅第一次 = 90 | 两次调用间 1090 保持 |
| z | 函数外,全局 | .data | 程序加载 | main/addConst 看到的是同一个 |
汇编层:名称重整 + .data 存放
清单 14-22 是静态局部变量的机器级真相23:
mov eax, DWORD PTR y.2319[rip] ## 读
add eax, 1000
mov DWORD PTR y.2319[rip], eax ## 写
……
.data
y.2319:
.long 90 ## static int y = INITy;三个看点:
- 住 .data:静态局部变量根本不在栈帧里——它要活过函数返回,栈做不到;
初值 90 用
.long写进可执行文件,加载时就在那里23; - 名称重整(name mangling):编译器把变量改名成
y.2319——同一个文件里 别的函数也可以有它自己的静态 y,靠改名区分。而且没加 .globl,所以别的文件 引用不到这个名字——「函数私有」就是这样实现的:不是权限检查,而是名字藏起来23; - 作者的手写版(清单 14-24)用
y_addConst这种有意义的重整名,比y.2319更可读,效果相同24。
收拢:程序组件的内存特点(表 14-1)
书里最后给了两张速查表。表 14-1 按「角色 × 内存段 × 访问权 × 生存期」归类25:
| 角色 | 内存段 | 访问权 | 生存期 |
|---|---|---|---|
| 自动局部变量 | 栈 | 读写 | 函数 |
| 常量 | 文本 | 只读 | 程序 |
| 指令 | 文本 | 只读 | 程序 |
| 静态局部变量 | 数据 | 读写 | 程序 |
| 全局变量 | 数据 | 读写 | 程序 |
「内存段」是操作系统创建的概念:常量进文本段是因为 OS 禁止写它; 变量必须住可写的数据段25。表 14-2 则汇总了控制存放位置的汇编器指令 (.text/.rodata/.data/.bss/.comm/.string/.ascii/.byte/.int/.long……), 细节见 as 的 info 文档26。这两张表是本章的自然终点: 「变量的一生」——定义在哪、存在哪、谁能读写、活多久——至此全部闭环。
7. 作者的判断与证据
| 说法 | 性质 |
|---|---|
| 17 条栈帧规则 | System V ABI 的教学化整理,书外标准,行为可验3 |
| 红区 128 字节、叶函数可用 | ABI 条款,书里解释并用于分析编译器输出12 |
| 全局变量在大程序中难管理 | 作者的经验判断,无定量证据5 |
| 「画栈帧图能避免错误」 | 作者自陈的个人经验(「多年前我学会了……」)17 |
| 按指针/按引用在汇编层相同 | 机制事实,由两种语言的调用约定推出11 |
| 跳表式 switch(第 13 章)与栈参数效率未量化 | 作者明示不做性能结论27 |
.int 与 .long 等价、个人偏好 .int | 工具事实 + 作者风格声明24 |
判断(我们的,不是书里的): 本章最实用的方法论是「给栈帧画图」—— 不是比喻,是真的拿纸笔画格子、标偏移。书里所有清晰的汇编讲解 (图 14-1/14-2/14-3/14-4)都是这么来的;反过来,调试别人代码时, 先画出当下的栈帧图,再对照 rbp 正负偏移读值,是少数能把「段错误」 变成「差 8 字节」的可操作手法。 如果错,会错在: 开优化后帧指针常被省略(第 11 章边界说过), 图的中心从 rbp 变成 rsp,且偏移随优化浮动——这套画法在 -O0 教学场景最准。
判断(我们的,不是书里的): 「名称重整实现信息隐藏」是本章最容易被低估的机制: 静态局部变量的「私有」不靠硬件保护,只靠编译器起了个没人知道的名字。 这和第 21 章特权级的「真硬件隔离」形成光谱的两端——软件纪律 vs 硬件机制, 可信程度差几个数量级。理解了这层,才会对「不要依赖混淆保安全」有直觉。 如果错,会错在: 有的实现(带内部符号表的调试构建)会保留原名信息, 「藏名」的隐蔽性比想象弱——但「不暴露为全局符号」这一层屏障始终存在。
8. 边界与局限
- 返回值只讲了 int(eax)。 更大结构体的返回规则(隐式传指针等)本书明确不深入11。
- 红区是 System V AMD64 的特性,Windows 的调用约定没有红区——跨平台汇编不能直接搬。
- 17 条规则是 -O0 教学口径。 优化编译器会省掉帧指针、让参数不进栈、用寄存器分配 打乱「存栈再用」的朴素模式;规则仍是协议,但输出的形态会认不出。
- 未涉及可变参数(va_arg)、结构体按值传递、浮点参数(xmm 寄存器)——这些是 ABI 的 后续章节,本书止于「int 与指针」。
- 名称重整的展示依赖 gcc 9.3.0;新版本编译器的重整名格式可能不同(如 y.0 之类),机制不变。
9. 可带走的
- 函数间数据交互四种:全局、输入、输出、更新;接口必须显式指定;
- C 四种作用域:文件、函数、块、函数原型;声明引名,定义分配内存;
- 全局变量住 .data(非 0 初值进文件)/ .bss(零初始化省文件空间),都经 rip 相对访问;
- extern 只声明不分配;头文件 + #ifndef 防止原型重复;
- 按值 = 副本;按指针 = 地址可改指;按引用 = 地址绑死——后两种汇编层无差别;
- 输出型参数的经典形态:
addTwo(x, y, &z),第三个参数按指针递地址; - 红区 = rsp 以下 128 字节,OS 不动,叶函数的免费临时仓库;
- 第 7 个起参数从右到左压栈;调用方负责清场(add rsp, 一次清);
- 栈参数用 rbp 正偏移(g=16、h=24、i=32 这类),局部变量用负偏移;
- push 前先想 16 字节对齐;压奇数个 8 字节要垫 8;
- 手写传栈参数更快的方式:一次 sub 整块 + .equ 命名偏移直存;
- 17 条栈帧规则 = System V ABI 教学版,读懂任何编译器输出的钥匙;
- 静态局部变量:作用域是函数、生存期是程序;住 .data,靠名称重整(y.2319)隐藏;
- 画栈帧图是设计/调试函数的第一动作;nop 是编译器的填充,不是你的代码。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 分而治之与数据接口 | 第14 章 剖析函数 | text/20-ch14.txt:1(搜「208 剖析函数」) · text/20-ch14.txt:15(搜「数据接口」) |
| 函数与用户输入/输出的区分 | 第14 章 剖析函数 | text/20-ch14.txt:50(搜「用户的数据输入」) · text/20-ch14.txt:54(搜「scanf」) |
| 四种数据交互 | 第14 章 剖析函数 | text/20-ch14.txt:59(搜「4 种方式」) |
| 四种作用域 | 第14 章 剖析函数 | text/20-ch14.txt:29(搜「4 种作用域」) · text/20-ch14.txt:39(搜「函数原型仅指」) |
| 全局变量与 extern | 第14 章 剖析函数 | text/20-ch14.txt:71(搜「extern 修饰符」) |
| .bss / .zero / .comm | 第14 章 剖析函数 | text/20-ch14.txt:108(搜「bss section」) · text/20-ch14.txt:122(搜「.comm」) · text/20-ch14.txt:98(搜「初始化为 0」) |
| 全局变量的 rip 相对访问 | 第14 章 剖析函数 | text/20-ch14.txt:139(搜「globals are relative to rip」) · text/20-ch14.txt:175(搜「紧挨着」) |
| 头文件与 #ifndef | 第14 章 剖析函数 | text/20-ch14.txt:184(搜「#ifndef」) |
| 按值/指针/引用传递 | 第14 章 剖析函数 | text/20-ch14.txt:253(搜「按值传递」) · text/20-ch14.txt:259(搜「按指针传递」) · text/20-ch14.txt:262(搜「是一样的」) |
| addTwo 主线例子 | 第14 章 剖析函数 | text/20-ch14.txt:285(搜「&z」) · text/20-ch14.txt:371(搜「address of z」) · text/20-ch14.txt:399(搜「清单 14-10」) |
| 红区与叶函数 | 第14 章 剖析函数 | text/20-ch14.txt:428(搜「红区」) · text/20-ch14.txt:430(搜「叶函数」) |
| nop 指令 | 第14 章 剖析函数 | text/20-ch14.txt:440(搜「nop —— 无操作」) |
| 9 参数调用(压栈与对齐) | 第14 章 剖析函数 | text/20-ch14.txt:460(搜「6 个以上的参数」) · text/20-ch14.txt:519(搜「for stack alignment」) · text/20-ch14.txt:528(搜「remove 3 ints」) |
| addNine 内部正偏移读参数 | 第14 章 剖析函数 | text/20-ch14.txt:592(搜「清单 14-15」) · text/20-ch14.txt:618(搜「from arg list」) |
| 直接存栈替代 push | 第14 章 剖析函数 | text/20-ch14.txt:648(搜「效率不太高」) · text/20-ch14.txt:703(搜「space for arguments」) |
| 画栈帧图的忠告 | 第14 章 剖析函数 | text/20-ch14.txt:640(搜「确切位置非常重要」) |
| 栈帧协议 17 条 | 第14 章 剖析函数 | text/20-ch14.txt:793(搜「规则如下」) · text/20-ch14.txt:822(搜「栈帧的整体内容」) |
| 静态局部变量动机 | 第14 章 剖析函数 | text/20-ch14.txt:864(搜「静态局部变量」) · text/20-ch14.txt:869(搜「被调用的次数」) |
| varLife 三变量对比 | 第14 章 剖析函数 | text/20-ch14.txt:877(搜「清单 14-18」) · text/20-ch14.txt:1003(搜「static int y」) |
| 名称重整 y.2319 | 第14 章 剖析函数 | text/20-ch14.txt:1043(搜「y.2319」) · text/20-ch14.txt:1073(搜「名称重整」) |
| 表 14-1/14-2 内存特点 | 第14 章 剖析函数 | text/20-ch14.txt:1218(搜「内存特点」) · text/20-ch14.txt:1233(搜「汇编器内存指令」) |
Footnotes
-
出处:「第14 章 剖析函数」第 8–16 段(
text/20-ch14.txt:1,搜「208 剖析函数」;text/20-ch14.txt:10,搜「显著的优势」;text/20-ch14.txt:15,搜「数据接口」)。 ↩ ↩2 -
出处:「第14 章 剖析函数」第 49–68 段(
text/20-ch14.txt:50,搜「用户的数据输入」;text/20-ch14.txt:59,搜「4 种方式」;text/20-ch14.txt:64,搜「如果被调用函数也知道数据」)。 ↩ ↩2 ↩3 -
出处:「第14 章 剖析函数」第 790–826 段(17 条规则,
text/20-ch14.txt:793,搜「规则如下」;栈帧内容图见第 820–826 段,text/20-ch14.txt:824,搜「不一定包含所有这些部分」;text/20-ch14.txt:826,搜「唯一始终存在的方框就是返回地址」)。 ↩ ↩2 ↩3 -
出处:「第14 章 剖析函数」第 26–47 段(
text/20-ch14.txt:29,搜「4 种作用域」;text/20-ch14.txt:32,搜「变量定义是一种」;text/20-ch14.txt:39,搜「函数原型仅指」;text/20-ch14.txt:46,搜「不必为参数使用不同的名称」)。 ↩ ↩2 ↩3 -
出处:「第14 章 剖析函数」第 69–72 段(
text/20-ch14.txt:71,搜「extern 修饰符」)与第 240–243 段(text/20-ch14.txt:241,搜「准确地跟踪」)。 ↩ ↩2 ↩3 -
出处:「第14 章 剖析函数」第 103–176 段(
text/20-ch14.txt:108,搜「bss section」;text/20-ch14.txt:115,搜「initialize to 0」;text/20-ch14.txt:122,搜「.comm z,4,4」;text/20-ch14.txt:139,搜「globals are relative to rip」;text/20-ch14.txt:175,搜「紧挨着」)。 ↩ ↩2 ↩3 -
出处:「第14 章 剖析函数」第 967–972 段(
text/20-ch14.txt:970,搜「非 0 值必须存储在可执行程序文件中」;text/20-ch14.txt:922,搜「.long」;text/20-ch14.txt:643,搜「你已经看到,函数直接在栈」)。 ↩ -
出处:「第14 章 剖析函数」第 219–239 段(
text/20-ch14.txt:228,搜「names are global」;text/20-ch14.txt:239,搜「相对于指令指针」)。 ↩ -
出处:「第14 章 剖析函数」第 189–196 段(
text/20-ch14.txt:189,搜「函数原型语句」;text/20-ch14.txt:184,搜「#ifndef」;text/20-ch14.txt:195,搜「防止原型语句重复」)。 ↩ ↩2 -
出处:「第14 章 剖析函数」第 266–324 段(
text/20-ch14.txt:285,搜「&z」;text/20-ch14.txt:303,搜「int *c」;text/20-ch14.txt:320,搜「*c = temp」)。 ↩ ↩2 -
出处:「第14 章 剖析函数」第 251–262 段(
text/20-ch14.txt:253,搜「按值传递」;text/20-ch14.txt:259,搜「按指针传递」;text/20-ch14.txt:262,搜「是一样的」;text/20-ch14.txt:255,搜「eax 寄存器」)。 ↩ ↩2 ↩3 ↩4 -
出处:「第14 章 剖析函数」第 399–443 段(
text/20-ch14.txt:408,搜「store a」;红区与叶函数见第 427–431 段,text/20-ch14.txt:428,搜「红区」;rbp/rsp 分工见第 434–436 段,text/20-ch14.txt:435,搜「相对于栈指针 rsp 定义」)。 ↩ ↩2 ↩3 ↩4 -
出处:「第14 章 剖析函数」第 439–443 段(
text/20-ch14.txt:440,搜「nop —— 无操作」;text/20-ch14.txt:442,搜「微调硬件实现细节」)。 ↩ -
出处:「第14 章 剖析函数」第 449–457 段(
text/20-ch14.txt:450,搜「6 个以上的参数」;text/20-ch14.txt:456,搜「从右到左的顺序入栈」;text/20-ch14.txt:643,搜「无须将其弹出」)。 ↩ ↩2 -
出处:「第14 章 剖析函数」第 460–554 段(
text/20-ch14.txt:487,搜「r9d」;text/20-ch14.txt:519,搜「for stack alignment」;text/20-ch14.txt:528,搜「remove 3 ints」;宽度问题见第 546–549 段,text/20-ch14.txt:547,搜「整个 64 位」)。 ↩ ↩2 ↩3 -
出处:「第14 章 剖析函数」第 592–638 段(
text/20-ch14.txt:618,搜「from arg list」;栈帧图见第 632–634 段,text/20-ch14.txt:174,搜「编译器为 main 函数」)。 ↩ ↩2 -
出处:「第14 章 剖析函数」第 640–641 段(
text/20-ch14.txt:640,搜「确切位置非常重要」)。 ↩ ↩2 -
出处:「第14 章 剖析函数」第 646–652 段(
text/20-ch14.txt:648,搜「效率不太高」)与第 655–735 段(text/20-ch14.txt:703,搜「space for arguments」;text/20-ch14.txt:717,搜「remove arguments」)。 ↩ -
出处:「第14 章 剖析函数」第 738–788 段(
text/20-ch14.txt:757,搜「seven,16」;text/20-ch14.txt:777,搜「plus seven」)。 ↩ -
出处:「第14 章 剖析函数」第 830–841 段(
text/20-ch14.txt:834,搜「writeStr 使用系统调用 write」;text/20-ch14.txt:836,搜「readLn 使用系统调用 read」;text/20-ch14.txt:838,搜「继续读取输入」)。 ↩ -
出处:「第14 章 剖析函数」第 864–875 段(
text/20-ch14.txt:867,搜「丢失」;text/20-ch14.txt:869,搜「被调用的次数」;text/20-ch14.txt:871,搜「两种可能的生存期」)。 ↩ -
出处:「第14 章 剖析函数」第 877–908 段(
text/20-ch14.txt:889,搜「int z = INITz」;text/20-ch14.txt:907,搜「5 个变量」)。 ↩ ↩2 -
出处:「第14 章 剖析函数」第 1025–1080 段(
text/20-ch14.txt:1043,搜「y.2319」;text/20-ch14.txt:1073,搜「名称重整」;text/20-ch14.txt:1060,搜「.data」)。 ↩ ↩2 ↩3 ↩4 -
出处:「第14 章 剖析函数」第 1158–1159 段(
text/20-ch14.txt:1158,搜「以小端序输出」)与第 1162–1217 段(text/20-ch14.txt:1186,搜「y_addConst」)。 ↩ ↩2 -
出处:「第14 章 剖析函数」第 1218–1230 段(
text/20-ch14.txt:1222,搜「内存段」;text/20-ch14.txt:1229,搜「由操作系统创建的段」;text/20-ch14.txt:1230,搜「禁止程序写入文本段」)。 ↩ ↩2 -
出处:「第14 章 剖析函数」第 1231–1254 段(
text/20-ch14.txt:1233,搜「汇编器内存指令」;text/20-ch14.txt:1249,搜「.ascii 则不会」;text/20-ch14.txt:1252,搜「执行相同的操作」)。 ↩ -
出处:「第13 章」跳表效率的讨论(
text/19-ch13.txt:888,搜「很难说跳表是否」)——本章的栈参数效率讨论同样止于定性。 ↩