这里的IT工程专业学生。我们被要求尝试上下文切换,一项特定的任务让我们实现一个相当粗糙的尝试/抛出系统。以下是我们一直在编写的代码:
struct ctx_s {
int esp;
int ebp;
};
struct ctx_s * pctx;
typedef int (func_t)(int); /* a function that returns an int from an int */
int try(func_t *f, int arg)
{
/* saving context by storing values of %esp and %ebp */
asm ("movl %%esp, %0"
: "=r"((*pctx).esp)
:
);
asm ("movl %%ebp, %0"
: "=r"((*pctx).ebp)
:
);
/* calling the function sent to try(), returning whatever it returns */
return f(arg);
}
int throw(int r)
{
printf("MAGIC PRINTn");
static int my_return = 0;
/* ^ to avoid "an element from initialisation is not a constant" */
my_return = r;
/* restituting context saved in try() */
asm ("movl %0, %%esp"
:
: "r"((*pctx).esp)
);
asm ("movl %0, %%ebp"
:
: "r"((*pctx).ebp)
);
/* this return will go back to main() since we've restored try()'s context
so the return address is whatever called try... */
/* my_return is static (=> stored in the heap) so it's not been corrupted,
unlike r which is now the second parameter received from try()'s context,
and who knows what that might be */
return my_return;
}
PCTX 是一个全局指针,指向一个包含两个 int 的简单结构,f 是一个调用 throw() 的函数,将一些返回代码 #define 发送到 42,main() 本质上分配 pctx,执行 result=try(f, 0) 并打印结果。我们预计结果为 42。
现在,您可能已经在throw()中发现了MAGIC PRINT。它在这里的原因不完全清楚;基本上,大多数(不是全部)学生在 throw() 内部存在分段错误;在这个函数中调用printf()使程序看起来工作正常,老师们认为任何系统调用也可以工作。
由于我并没有真正得到他们的解释,我尝试比较两个版本(有和没有printf())的gcc -S生成的汇编代码,但我无法做太多。在 throw() 的左大括号(第 33 行)处设置断点并使用 gdb 进行反汇编给了我这个:
没有 printf():
Breakpoint 1, throw (r=42) at main4.c:38
(gdb) disass
Dump of assembler code for function throw:
0x0804845a <throw+0>: push %ebp
0x0804845b <throw+1>: mov %esp,%ebp
0x0804845d <throw+3>: mov 0x8(%ebp),%eax
0x08048460 <throw+6>: mov %eax,0x8049720
0x08048465 <throw+11>: mov 0x8049724,%eax
0x0804846a <throw+16>: mov (%eax),%eax
0x0804846c <throw+18>: mov %eax,%esp
0x0804846e <throw+20>: mov 0x8049724,%eax
0x08048473 <throw+25>: mov 0x4(%eax),%eax
0x08048476 <throw+28>: mov %eax,%ebp
0x08048478 <throw+30>: mov 0x8049720,%eax
0x0804847d <throw+35>: pop %ebp
0x0804847e <throw+36>: ret
End of assembler dump.
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0xb7e846c0 in ?? ()
使用 printf():
Breakpoint 1, throw (r=42) at main4.c:34
(gdb) disassemble
Dump of assembler code for function throw:
0x0804845a <throw+0>: push %ebp
0x0804845b <throw+1>: mov %esp,%ebp
0x0804845d <throw+3>: sub $0x18,%esp
0x08048460 <throw+6>: movl $0x80485f0,(%esp)
0x08048467 <throw+13>: call 0x8048364 <puts@plt>
0x0804846c <throw+18>: mov 0x8(%ebp),%eax
0x0804846f <throw+21>: mov %eax,0x804973c
0x08048474 <throw+26>: mov 0x8049740,%eax
0x08048479 <throw+31>: mov (%eax),%eax
0x0804847b <throw+33>: mov %eax,%esp
0x0804847d <throw+35>: mov 0x8049740,%eax
0x08048482 <throw+40>: mov 0x4(%eax),%eax
0x08048485 <throw+43>: mov %eax,%ebp
0x08048487 <throw+45>: mov 0x804973c,%eax
0x0804848c <throw+50>: leave
0x0804848d <throw+51>: ret
End of assembler dump.
(gdb) c
Continuing.
MAGIC PRINT
result = 42
Program exited normally.
我真的不知道该怎么做。显然事情正在以不同的方式发生,但我发现很难理解这两种情况下发生了什么...... 所以我的问题是,本质上:调用 printf 如何使投掷不偏执?
好的,这是一个有点松散的分析,因为我看不到 try 部分,但从标准调用约定来看,包含 try 的方法将节省%esp
%ebp
,减少%esp
为局部变量腾出空间并运行节省%esp
和%ebp
的"try"代码。
通常,当函数退出时,它会在返回之前使用leave
还原这些更改。离开将恢复%ebp
%esp
,弹出%ebp
并返回。这可确保%esp
恢复到为局部变量保留空间之前的位置。
没有printf
的版本的问题在于,它不使用leave
,如果不先将其内容恢复到%esp
,就会弹出%ebp
。ret
指令将弹出一个局部变量并返回到该变量。不是最好的结果。
我的怀疑是,由于您的函数没有局部变量,编译器认为没有理由从%ebp
恢复%esp
。由于printf
在堆栈上保留空间,因此编译器在该版本中知道必须在返回之前还原%esp
。
如果要测试理论,只需编译到汇编,替换;
0x0804847d <throw+35>: pop %ebp
带有休假指示并组装结果。它应该也能正常工作。
或者,我怀疑您可以在 asm 指令中向 gcc 指出%esp
已被破坏,从而让它产生休假。
编辑:显然将%esp
标记为破坏本质上是 gcc 中的一个 NOOP :-/
您正在将ESP
"恢复"为保存在另一个函数中的值。这里可能不是一个有用的值。
与"魔术"代码的区别在于,它使编译器在throw
函数中保存和恢复堆栈帧。
最后的leave
指令相当于
mov %ebp, %esp
pop %ebp
这可能会使堆栈指针返回到函数条目处的状态。