今天一个技术群里有人发了一个脉脉上的面试题,如下:
这个题如果只用大学的C语言知识肯定无法解决的。还好我以前看过《深入理解计算机系统》这本书,知道一个程序其实就是一堆地址和一堆指令组成,这个提明显需要在子函数里面修改父函数的栈地址。我们知道栈地址是从高地址往的地址分配内存的,如下图所示:
在做函数跳转的时候,有两个很关键的寄存器,ebp(64位是rbp)和esp(64位是rsp)。ebp是保存的一个函数的栈基地址,esp是保存的当前执行代码的地址。也就是所有函数里面分配的临时变量都会在ebp和esp记录的地址之间。做函数跳转的时候,首先会保存父函数的当前栈帧地址,也就是ebp寄存器的值,然后把ebp切换到当前的esp,开始执行新的函数代码。
有了这些基础知识,思路也就有了:我们需要在pass函数里面先获取到当前ebp的值,然后倒推上一个函数的esp的值,然后给减去分配临时变量的偏移量,赋值为456即可。
下面开始解这个题:
先把如下C语言代码编译后,生成汇编代码:
#include <stdio.h>
void pass()
{
}
int main()
{
int x = 123;
pass();
printf("%d\n", x);
return 0;
}
objdump -D a.out之后main函数和pass函数如下:
080483c4 <pass>:
80483c4: 55 push %ebp
80483c5: 89 e5 mov %esp,%ebp
80483c7: 5d pop %ebp
80483c8: c3 ret
080483c9 <main>:
80483c9: 55 push %ebp
80483ca: 89 e5 mov %esp,%ebp
80483cc: 83 e4 f0 and $0xfffffff0,%esp
80483cf: 83 ec 20 sub $0x20,%esp
80483d2: c7 44 24 1c 7b 00 00 movl $0x7b,0x1c(%esp)
80483d9: 00.
80483da: e8 e5 ff ff ff call 80483c4 <pass>
80483df: b8 c4 84 04 08 mov $0x80484c4,%eax
80483e4: 8b 54 24 1c mov 0x1c(%esp),%edx
80483e8: 89 54 24 04 mov %edx,0x4(%esp)
80483ec: 89 04 24 mov %eax,(%esp)
80483ef: e8 00 ff ff ff call 80482f4 <printf@plt>
80483f4: b8 00 00 00 00 mov $0x0,%eax
80483f9: c9 leave
80483fa: c3 ret
这里可以通过“movl $0x7b,0x1c(%esp)”看到变量x的地址是esp地址加上0x1c。而由于产生了函数调用,并且在pass函数里面需要保存ebp的地址,所以这里esp会往下偏移8个地址(原因是call指令和push指令都会做一次入栈操作)。我们通过汇编获取到当前函数的ebp就可以计算出来上一个函数的esp寄存器的值了。所以我们最终的解题代码如下:
#include <stdio.h>
void pass()
{
int ebp = 0;
asm("movl %%ebp, %0 \n\t":"=r"(ebp));
int *px = (int*) (ebp +0x1c + 0x8);
*px = 456;
}
int main()
{
int x = 123;
pass();
printf("%d\n", x);
return 0;
}
需要注意的是以上的方法只能够在32位系统上面正确运行,如果在64位系统上面,需要操作的是rbp,并且地址需要使用64位地址来存放。