这篇文档主要设计如下几个知识点:小程序
注意 除非在使用gcc编译代码的时候说明使用了编译器优化参数-O
,其它全部的gcc命令都不使用编译器优化。sass
刚入门写下下面的代码很常见,虽然是错误的写法(ch指针没有初始化),可是程序的却能正常的执行。bash
#include <stdio.h> int main() { long i; long j; char *ch; scanf("%s", ch); }
可是在scanf
语句下在给变量i
和j
进行初始化话,程序却不能正确的执行了,在执行scanf
后会抛出SegmentFault异常。函数
#include <stdio.h> int main() { long i; long j; char *ch; scanf("%s", ch); i = 0; j = 0; }
更奇怪的是,若是将i
或者j
注释掉其中的一个,程序又能正常的执行:优化
#include <stdio.h> int main() { long i; long j; char *ch; scanf("%s", ch); i = 0; //j = 0; }
为了搞明白为何会出现这种状况,须要使用gdb来调试这两段代码。spa
gcc -O0 -g main.c -o main.o gdb main.o
进入gdb调试界面,首选使用disassemble
来反汇编函数main
,这样能够帮助设置断点和查看编译后的汇编指令。设计
注释掉j
的gdb调试界面,版本一:指针
(gdb) disassemble main Dump of assembler code for function main: 0x0000000000400546 <+0>: push %rbp 0x0000000000400547 <+1>: mov %rsp,%rbp 0x000000000040054a <+4>: sub $0x10,%rsp 0x000000000040054e <+8>: mov -0x10(%rbp),%rax 0x0000000000400552 <+12>: mov %rax,%rsi 0x0000000000400555 <+15>: mov $0x400604,%edi 0x000000000040055a <+20>: mov $0x0,%eax 0x000000000040055f <+25>: callq 0x400430 <__isoc99_scanf@plt> 0x0000000000400564 <+30>: movq $0x0,-0x8(%rbp) 0x000000000040056c <+38>: mov $0x0,%eax 0x0000000000400571 <+43>: leaveq 0x0000000000400572 <+44>: retq
没有注释掉i
和j
的gdb调试界面,版本二:调试
(gdb) disassemble main Dump of assembler code for function main: 0x0000000000400546 <+0>: push %rbp 0x0000000000400547 <+1>: mov %rsp,%rbp 0x000000000040054a <+4>: sub $0x20,%rsp 0x000000000040054e <+8>: mov -0x18(%rbp),%rax 0x0000000000400552 <+12>: mov %rax,%rsi 0x0000000000400555 <+15>: mov $0x400604,%edi 0x000000000040055a <+20>: mov $0x0,%eax 0x000000000040055f <+25>: callq 0x400430 <__isoc99_scanf@plt> 0x0000000000400564 <+30>: movq $0x0,-0x10(%rbp) 0x000000000040056c <+38>: movq $0x0,-0x8(%rbp) 0x0000000000400574 <+46>: mov $0x0,%eax 0x0000000000400579 <+51>: leaveq 0x000000000040057a <+52>: retq
观察两段代码的汇编指令。只有在0x000000000040054e
处代码不同:code
mov -0x10(%rbp),%rax
表示将地址%rbp-0x10
处的值传递到%rax
寄存器mov -0x18(%rbp),%rax
表示将地址%rbp-0x18
处的值传递到%rax
寄存器而后在执行mov %rax %rsi
,表示将寄存器$rxa
的值传递给寄存器%rsi
,%rsi
表示函数调用时的第二个参数,当调用scanf("%s", ch)
的时候,ch
的值就是%rsi
的值。既然程序执行的时候在scanf
报错,那么源头就是%rsi
的值不同。那么就继续回到gdb界面,在调用scanf
的以前设置断点(这里选择地址0x0000000000400552
处),观察一下%rsi
的值究竟是什么。
首选设置断点,而后执行程序:
(gdb) b *0x000000000040055f (gdb) run
而后执行info register rsi
指令,来打印$rsi
的值:
rsi 0x7fffffffddf0 140737488346608
rsi 0x400450 4195408
程序执行的结果表面,地址0x7fffffffddf0
是合法的,0x400450
是非法的。在执行0x000000000040055f
(scanf("%s", ch)
)的时候两个版本会将标准输入分别写入以地址0x7fffffffddf0
和0x400450
为始的连续内存空间上。
为何地址0x400450
是非法的?来看一下Linux x86-64运行时的内存镜像:
地址0x400450
在Read-only code segment区域,因此对这块地址进行写操做是非法的。在运行时环境中,只能对stack
和heap
进行写操做。
为何rsi
地址在两个版本下的值差异这么大,一个在高地址内存空间,而另外一个在低地址内存空间?在执行main
函数以前,运行时已经在栈上进行了入栈出栈的操做了,地址-0x10(%rbp)
和-0x18(%rbp)
的值是上一次入栈时写入的数据,由于没有进行初始化,因此还保留了上一次操做的值(我的臆测)。