這篇文檔主要設計如下幾個知識點:小程序
注意 除非在使用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)
的值是上一次入棧時寫入的數據,由於沒有進行初始化,因此還保留了上一次操做的值(我的臆測)。