一道有趣的面试题
.
.
.
.
.
同事问了我一道有意思的面试题,经过一番琢磨,解出了答案,遂把原题和我的答案记录如下:
问题:void f(void) 如何实现,可以打印出 x 是任何一个值?
1 int main(int argc, char** argv) 2 { 3 int x = 10; 4 f(); 5 printf("x = %d\n", x); 6 return 0; 7 }
我提供了两种解题思路,需要的前置知识如下:
思路1:Unix 文件IO。对此前置知识感兴趣的同学可以参考我之前的文章《 一起学 Unix 环境高级编程 (APUE) 之 文件 IO 》。
思路2:x86 汇编。
思路1:
在 f() 函数里面随意打印一个值,然后把标准输出(stdout)重定向到 /dev/null,让后面的代码无法 printf(3) 到 console 上。
1 #include <fcntl.h> 2 #include <stdio.h> 3 #include <unistd.h> 4 5 #include <sys/stat.h> 6 #include <sys/types.h> 7 8 void f(void) 9 { 10 int fd = -1; 11 printf("3\n"); 12 if ((fd = open("/dev/null", O_WRONLY)) < 0) 13 { 14 perror("Open /dev/null failed!"); 15 return ; 16 } 17 dup2(fd, 1); // stdout 的文件描述符是1。 18 close(fd); 19 }
解释:由于 printf(3) 会将参数输出到标准输出(stdout)流,通过 dup2(2) 函数将 stdout 关闭,并将 /dev/null 的文件描述符拷贝到 1 号文件描述符(stdout 的文件描述符是1),就可以使 printf(3) 向 1 号文件描述符的输出全都重定向到 /dev/null 文件中。/dev/null 是一个像黑洞一样的特殊文件,所有写入 /dev/null 的内容都会消失,因此 dup2(2) 之后所有 printf(3) 的内容都将不可见。
同事说这种实现方式有点诡异,也许不是题意的目的,题目是希望能够通过 f() 函数修改 main() 函数的局部变量 x 的值,那么接下来我们来看看第二种方案。
思路2:
取出 main() 的栈指针地址 rbp,然后给 rbp-4 的地址重新赋值就可以了。
1 void f(void) 2 { 3 asm volatile( 4 "popq %%rbp;\n\t" 5 "movl $3, -4(%%rbp);\n\t" 6 "ret;\n\t" 7 ::: 8 ); 9 }
先看下汇编代码是如何存储 main() 函数的局部变量 x 的。
>$ gcc -Wall -S 2.c >$ cat -n 2.s
以下是摘录的 main() 函数代码:
34 main: 35 .LFB1: 36 .cfi_startproc 37 pushq %rbp 38 .cfi_def_cfa_offset 16 39 .cfi_offset 6, -16 40 movq %rsp, %rbp ; 记录 main() 函数栈帧开始的地址 41 .cfi_def_cfa_register 6 42 subq $32, %rsp ; 分配 32byte 的栈空间 43 movl %edi, -20(%rbp) 44 movq %rsi, -32(%rbp) 45 movl $10, -4(%rbp) ; 将变量 x 的值入栈 46 call f ; 调用 f() 函数 47 movl -4(%rbp), %eax 48 movl %eax, %esi 49 leaq .LC0(%rip), %rdi 50 movl $0, %eax 51 call printf@PLT 52 movl $0, %eax 53 leave 54 .cfi_def_cfa 7, 8 55 ret 56 .cfi_endproc
说明:
1. 第40行将栈帧开始的地址记录到 rbp 寄存器;
2. 第42行移动栈指针(rsp),为 main() 函数分配了 32byte 的栈空间用于存储局部变量;
3. 第45行将常数 10 存放到 rbp-4 的位置。
接下来再来看下 f() 函数所做的处理:
5 f: 6 .LFB0: 7 .cfi_startproc 8 pushq %rbp 9 .cfi_def_cfa_offset 16 10 .cfi_offset 6, -16 11 movq %rsp, %rbp ; 记录 f() 函数栈帧开始的地址 12 .cfi_def_cfa_register 6 13 #APP 14 # 5 "2.c" 1 ; APP 到 NO_APP 之间是我们自己写的指令 15 popq %rbp; ; 弹栈,得到 main() 函数栈帧的起始地址 16 movl $3, -4(%rbp); ; 修改 main() 函数局部变量 x 的值。 ; 还记得上面为 x 赋值的时候用的是哪个地址吗?
; 就是 main() 函数的栈帧里 rbp-4 的位置。 17 ret; ; 直接返回,不要再执行编译器生成的弹栈操作了。 18 19 # 0 "" 2 20 #NO_APP 21 nop 22 popq %rbp 23 .cfi_def_cfa 7, 8 24 ret 25 .cfi_endproc
注释中已经解释得很清楚了,原理就是先找到 main() 函数的栈帧的位置,再找到变量 x 所在的位置,最后通过指针修改变量 x 所在位置的值就可以了。
作者:dybai
出自:https://0xcafebabe.cnblogs.com
赞赏:3Ky9q5HVGpYseBPAUTvbJBvM3h3FQ3edqr(BTC)
本作品采用知识共享署名-相同方式共享 3.0 中国大陆许可协议进行许可。
欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
posted on 2019-02-22 17:08 0xCAFEBABE 阅读(295) 评论(0) 编辑 收藏 举报