考虑以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <stdio.h>
void other(void (*funcp)()) { funcp(); }
void outer(void) { int a = 0x0; a += 0x22;
void inner(void) { a += 0x33; }
other(inner); printf("a is 0x%x \n", a); }
int main() { outer(); return 0; }
|
这段代码的特别之处在于,inner函数定义在outer函数内部,且inner函数访问了outer函数的局部变量。
局部变量处于outer函数的栈上,而inner函数的调用时机并不确定,因此inner函数自身的栈与该局部变量的距离并不固定,需要通过某种办法将该局部变量的地址传递给inner函数。GCC解决这个问题的办法,就是在栈上生成了代码。
编译该代码,出现了以下warning
/usr/bin/ld: warning: /tmp/ccRfPclj.o: requires executable stack (because the .note.GNU-stack section is executable)
关于该warning,详情见Referfence-1。
编译之后,通过反汇编查看outer函数和inner函数的实现,可以发现outer函数有很长的一段逻辑,是往栈中写入了特定的内容,相关实现截取如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 40115d: 48 8d 45 10 lea 0x10(%rbp),%rax 401161: 48 89 45 f0 mov %rax,-0x10(%rbp) 401165: 48 8d 45 d0 lea -0x30(%rbp),%rax 401169: 48 83 c0 04 add $0x4,%rax 40116d: 48 8d 55 d0 lea -0x30(%rbp),%rdx 401171: b9 40 11 40 00 mov $0x401140,%ecx 401176: 66 c7 00 41 bb movw $0xbb41,(%rax) 40117b: 89 48 02 mov %ecx,0x2(%rax) 40117e: 66 c7 40 06 49 ba movw $0xba49,0x6(%rax) 401184: 48 89 50 08 mov %rdx,0x8(%rax) 401188: c7 40 10 49 ff e3 90 movl $0x90e3ff49,0x10(%rax) 40118f: b8 00 00 00 00 mov $0x0,%eax 401194: 89 45 d0 mov %eax,-0x30(%rbp) 401197: 8b 45 d0 mov -0x30(%rbp),%eax 40119a: 83 c0 22 add $0x22,%eax 40119d: 89 45 d0 mov %eax,-0x30(%rbp) 4011a0: 48 8d 45 d0 lea -0x30(%rbp),%rax 4011a4: 48 83 c0 04 add $0x4,%rax 4011a8: 48 89 c7 mov %rax,%rdi 4011ab: e8 76 ff ff ff call 401126 <other>
|
这一段的实现比较晦涩,通过GDB追踪outer函数的执行,当执行到other函数时,并没有直接调用inner函数,而是跳入了栈上的某一个地址,对该地址进行反汇编,出现了以下代码:
1 2 3
| 0x7fffffffe034: mov $0x401140,%r11d 0x7fffffffe03a: movabs $0x7fffffffe030,%r10 0x7fffffffe044: rex.WB jmp *%r11
|
这段栈上的代码做了两件事情,传递了inner函数以及局部变量的真实地址,接着跳入inner函数执行,outer函数那一堆操作栈的代码就是在栈上生成该代码。
而inner函数的实现特别之处如下:
1 2 3 4 5
| 401144: 4c 89 d0 mov %r10,%rax 401147: 4c 89 55 f8 mov %r10,-0x8(%rbp) 40114b: 8b 10 mov (%rax),%edx 40114d: 83 c2 33 add $0x33,%edx 401150: 89 10 mov %edx,(%rax)
|
inner函数从r10寄存器读取了局部变量的值,并完成了修改,这种读取行为跟生成的栈上代码是强相关的。
总结
GCC为了支持这种在函数中定义函数的用法,在栈上生成了代码,这种行为是极度不安全的。
Reference
- Linker’s warnings