- 论坛徽章:
- 0
|
回复 #17 MMMIX 的帖子
随便写了一点探讨一下:
-O2副作用分析一例
-213@163.com[/email]
问题来源:
深入理解计算机系统(修订版) A.K.A CS:APP
中国电力出版社
[美]Randal E. Bryant & David O'Hallaron 著
[中]龚弈利 & 雷迎春 译
pp230 练习题 4.5
该题目测试x86指令popl %esp的语义为以下两种情况的哪一种
(1)将%esp置为从存储器中读出的值
(2)将%esp置为(%esp) + $4
测试代码为
/*** poptest.c ***/
int poptest(int tval)
{
int rval;
asm("push1 %1; movl %%esp, %%edx; popl %%esp;\
movl %%esp, %0; movl %%edx, %%esp"
: "=r" (rval)
: "r" (tval)
: "edx");
return rval;
}
/*** main.c ***/
int main()
{
if (1 == poptest(1)) {
printf("semantics(1)\n");
}
else {
printf("semantics(2)\n");
}
return 1;
}
先稍微解释下该代码的意图:
tval压栈
保存%esp到%edx以防测试结果为情况(1)时栈崩溃
popl %esp
将%esp的值保存到rval
从%edx还原%esp
这样如果poptest调用的返回与参数相同则说明popl %esp遵循语义(1)
否则(2)
测试结果为
$gcc poptest.c main.c
./a.out
semantics(1)
加上-O2优化
$gcc -O2 poptest.c main.c
./a.out
segmentation fault
这说明gcc又犯了经验主义的过渡优化错误,下面我们分析一下它是如何过渡优化的:
$gcc -O2 -S poptest.c
$cat poptest.s
...
...
push1 %ebp
movl %esp, %ebp
movl 8(%esp), %eax
leave
#APP
push1 %eax
movl %esp, %edx
popl %esp
movl %esp, %eax
movl %edx, %esp
#NO_APP
ret
很明显gcc自作聪明的把我们的嵌入汇编插入到leave/ret中间去,破坏了正确的堆栈操作过程。
在执行ret指令时,堆栈的情况为:
%ebp --> +-----------+
| |
| |
| |
| |
+-----------+
| tval |
+-----------+
| 返回地址 |
+-----------+ <-- %esp (这里是没有-O2选项时%esp的正确位置)
| tval |
+-----------+ <-- %esp (因为将leave/ret拆开造成ret前%esp指向错误位置)
| |
| |
~ ~ ~ ~ ~
| |
| |
我们知道leave指定等价于
movl %ebp, %esp
popl %ebp
它的作用是恢复调用者的帧指针,并且使栈指针指向返回地址,然后ret正确执行返回。
但经过gcc这样一优化在ret之前我们的栈指针并未指向正确的返回地址,而是tval的值,
程序当然会听话的跳转到tval地址处执行,结果是不可避免的segmentation fault.
为求信息的完全,下面附上未优化时的汇编代码:
push1 %ebp
movl %esp, %ebp
sub1 $4, %esp
movl 8(%ebp), %eax
#APP
同上
#NOAPP
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
参考文献:
1. 深入理解计算机系统(修订版) 中国电力出版社
2. GNU binutils GAS文档 |
|