phase_2:
-
汇编码分析:
0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp //开栈分配4*6 40字节
400f02: 48 89 e6 mov %rsp,%rsi //栈顶地址存入%rsi
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> //调用函数读取6个数
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) //比较1与rsp中的数是否一样,一样就继续
400f0e: 74 20 je 400f30 <phase_2+0x34> //一样
400f10: e8 25 05 00 00 callq 40143a <explode_bomb>//不一样 bomb!
400f15: eb 19 jmp 400f30 <phase_2+0x34>
400f17: 8b 43 fc mov -0x4(%rbx),%eax // 第一个数存进%eax
400f1a: 01 c0 add %eax,%eax //左移一位,*2
400f1c: 39 03 cmp %eax,(%rbx) //比较前一个数是否为%rbx中后一个数的2倍
400f1e: 74 05 je 400f25 <phase_2+0x29> //是 跳转到0x400f25
400f20: e8 15 05 00 00 callq 40143a <explode_bomb> //不是 bomb!
400f25: 48 83 c3 04 add $0x4,%rbx //%rbx后移4字节 变成下一个数
400f29: 48 39 eb cmp %rbp,%rbx //比较最后一个数和%rbp中的数是否相等
400f2c: 75 e9 jne 400f17 <phase_2+0x1b> //是 循环一遍之前的
400f2e: eb 0c jmp 400f3c <phase_2+0x40> //不是 跳到0x400f3c 结束循环
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx //%rsp中偏移4字节 将第二个数字存入%rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp //%rsp再偏移24个字节,第7个数存进%rsp
400f3a: eb db jmp 400f17 <phase_2+0x1b> //跳转进循环
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq
答案:1 2 4 8 16 32
等效代码:
void phase_2() {
int numbers[6];
read_six_numbers(numbers); // 读取6个数
int* end = &numbers[6]; // 计算“第七个数”的地址(实际未存储数据)
for (int* ptr = &numbers[1]; ptr < end; ptr++) {
// 验证每个数是否为前一个数的两倍
}
}
问题思考
-
为什么看起来像 “存了第七个数”?
“第七个数” 实际是一个内存地址标记,而非真正存储了第七个数值。
关键代码分析
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp ; %rbp = &numbers[6](第七个数的位置)
...
400f29: 48 39 eb cmp %rbp,%rbx ; 比较当前位置是否到达%rbp
400f2c: 75 e9 jne 400f17 ; 若未到达,继续循环
为什么需要这个地址?
-
循环终止条件:
函数使用%rbp
作为循环的终止标记。虽然用户只输入 6 个数(numbers[0]~numbers[5]
),但通过计算0x18(%rsp)
(即&numbers[6]
),函数创建了一个边界地址。当%rbx
递增到这个地址时,表示所有 6 个数都已验证完毕。 -
内存布局:
栈上分配的空间为 24 字节(6 个整数),但函数通过lea
计算了一个超出该区域的地址(%rsp + 24
)。这个地址本身未被写入数据,但作为指针比较的终点。 -
安全设计:
这种方式避免了硬编码循环次数(如cmp $6, %ecx
),而是通过指针比较动态确定循环终止点,使代码更健壮。
这是一种常见的编程技巧,称为哨兵值(Sentinel)或边界标记:
- 函数并未真正存储第七个数,而是计算了一个超出有效数据范围的地址。
- 这个地址仅用于比较,不参与数据验证,因此不会导致越界访问。
为什么40 字节的栈空间看似可容纳 10 个整数(每个整数 4 字节),但实际只需存储 6 个整数(24 字节)。剩余 16 字节可能用于对齐或临时变量?
内存对齐 和 临时变量存储 的设计考量:
1. 栈空间分配的基本规则
在 x86-64 架构中,栈操作通常遵循以下原则:
- 对齐要求:栈指针必须始终保持 16 字节对齐(部分指令如 SSE 浮点运算强制要求)。
- 函数调用约定:调用函数前,栈指针需调整为 16 的倍数,以满足被调用函数的对齐需求。
2. 具体案例分析
400efe: 48 83 ec 28 sub $0x28,%rsp ; 分配40字节栈空间(0x28 = 40)
- 用户输入:6 个整数需要 24 字节(6 × 4 = 24)。
- 实际分配:40 字节(超过需求 16 字节)。
3. 为什么多分配 16 字节?
① 内存对齐需求
- 初始状态:假设调用
phase_2
前栈指针%rsp
为 16 的倍数(如0x7fffffffe000
)。 - 函数调用开销:
callq
指令会将返回地址压栈(8 字节),导致%rsp
变为 非 16 字节对齐(如0x7fffffffdff8
)。 - 调整栈指针:
为满足 16 字节对齐,需分配 40 字节(而非 24 字节),使%rsp
调整为:plaintext
0x7fffffffdff8 - 40 = 0x7fffffffdfe0 → 16的倍数(0x7fffffffdfe0 ÷ 16 = 整数)
② 临时变量存储
- 函数可能需要额外空间存储中间值,例如:
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers>
read_six_numbers
可能将输入解析到栈上的临时缓冲区,再复制到最终位置。
4. 内存布局示例
栈地址(低到高) 用途
0x7fffffffdfe0 ──────────────┐
│
... │ 40字节栈空间
│
0x7fffffffe008 ──────────────┘
↑ ↑
|- %rsp(调整后) |- %rsp(调用前)
↑
|- 返回地址(8字节)
5. 为什么不精确分配 24 字节?
如果只分配 24 字节:
sub $0x18,%rsp ; 24字节(0x18 = 24)
- 调整后的
%rsp
为:0x7fffffffdff8 - 24 = 0x7fffffffdfe8 → 非16的倍数!
- 这会导致后续指令(如调用其他函数)因对齐错误而性能下降或崩溃。
总结
- 多分配的 16 字节主要用于:
- 满足 16 字节对齐要求,确保栈指针在函数执行期间保持对齐。
- 存储临时变量或中间结果(如函数调用时的参数传递)。
- 这种设计虽然浪费了部分栈空间,但提高了内存访问效率,避免了潜在的对齐错误。