某次面试时,跟面试官说,函数可以返回局部变量,被指,不懂C++,不懂操作系统,事后写代码验证了一下。。后来与另一个面试官讨论到这个问题时,他说,访问肯定是行的,只是数据还是不是你想要的数据。觉得这话在理。。这个东西也不一定有什么用处,就当是了解吧~~
如果直接在linux的gcc上执行下图代码
gcc –o test test.c
./test
会直接输出(null)。如果将printf(“%s”,fun())换成printf(“%s\n”,fun())就直接报Segmentation fault (core dumped)。具体细节不是很清楚,不过加了换行的printf会被直接替换成puts(可以在相应的汇编代码中查看到)。
在读汇编代码之前,先说下栈帧(图片来源:Linux内核完全剖析),虽然这个结构的细节只对很多年前的编译器有用,但,这个结构现在还是一样的。调用者调用函数之前,先按从右到左的顺序放入参数,然后保存一些东西,然后进入被调用函数开始执行。
把这段代码转成汇编,代码是64位的,很多指令的后缀可能不常见
gcc –S –o test.s test.c
得到:(代码有点长,其中其他的printf只是用来找规律的,部分不重要内容已删除)
.file "test.c"
.section .rodata
.LC0:
.string "%s" # 为了方便查看输出结果,可以改成 .string "%s\n"
.text
.globl fun
.type fun, @function
fun:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
#表示执行完上条指令后,当前栈指针与调用该函数时的栈指针相差了16字节,计算hello在栈中地址时可以用到
# 详见http://sourceware.org/binutils/docs/as/CFI-directives.html 和http://blog.csdn.net/jtli_embeddedcv/article/details/9321253
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp #给变量开辟空间
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax # eax清零
movl $1819043176, -16(%rbp) #放入字符串hello的地址值
movw $111, -12(%rbp)
leaq -16(%rbp), %rax # 把 -16(%rbp)中的量相对段基地址的偏移量放入rax
movq %rax, %rsi # 把rax放入rsi寄存器中,rsi是printf调用的参数。由两个printf调用推断出的
movl $.LC0, %edi #.LC0是printf的格式串
movl $0, %eax # eax保存函数的返回值,调用函数前先清零
call printf
movl $0, %eax
movq -8(%rbp), %rdx # dx寄存器,在输入输出指令中存放端口值
xorq %fs:40, %rdx
je .L3
call __stack_chk_fail
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
movl $1819043144, -16(%rbp)
movw $111, -12(%rbp)
leaq -16(%rbp), %rax
movq %rax, %rsi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
call fun
movq %rax, %rsi # 直接的汇编代码中,rax没有设置值,与上面两个printf相比少了一条语句
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
movq -8(%rbp), %rdx
je .L6
call __stack_chk_fail
xorq %fs:40, %rdx
在最后一个printf加入一条与前两个printf一样的leaq语句,就可以输出,还在栈中的局部变量(buf)所指向的值。问题是,buf在栈中的地址偏移当前的rbp多少呢?由两个函数中的
movq %rsp, %rbp
subq $16, %rsp
可知,每个函数的局部变量占了16字节。并且偏移最远的4个字节存放了buf,即局部变量的地址。再由fun函数的.cfi_def_cfa_offset 16 可知,在fun存放16字节局部变量前,与主函数的call fun语句相差16个字节。所以,buf与rbp相差16*3=48字节。在printf的红色语句前面,加入
leaq -48(%rbp),%rax
再链接.s文件得到可执行文件
gcc –o test test.s
输出:
所以,函数能返回局部变量的指针,栈中的内容能被访问到,只是,编译器不允许。
至于堆,还没想到要怎么弄才可以。数据当然是存在的,能不能绕过操作系统的保护访问到,才是问题的根本。