C入門小程序

C入門小程序

這篇文檔主要設計如下幾個知識點:小程序

  • C語言的指針
  • gcc和gdb的簡單使用
  • 簡單的彙編指令
  • Linux環境下C程序運行時的棧幀

奇怪的代碼

注意 除非在使用gcc編譯代碼的時候說明使用了編譯器優化參數-O,其它全部的gcc命令都不使用編譯器優化。sass

剛入門寫下下面的代碼很常見,雖然是錯誤的寫法(ch指針沒有初始化),可是程序的卻能正常的執行。bash

#include <stdio.h>

int main() {
    long i;
    long j;
    char *ch;
    scanf("%s", ch);
}

可是在scanf語句下在給變量ij進行初始化話,程序卻不能正確的執行了,在執行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;
}

DEBUG

爲了搞明白爲何會出現這種狀況,須要使用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

沒有註釋掉ij的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))的時候兩個版本會將標準輸入分別寫入以地址0x7fffffffddf00x400450爲始的連續內存空間上。

爲何地址0x400450是非法的?來看一下Linux x86-64運行時的內存鏡像:

Linux x86-64 rum-time memory image

地址0x400450在Read-only code segment區域,因此對這塊地址進行寫操做是非法的。在運行時環境中,只能對stackheap進行寫操做。

爲何rsi地址在兩個版本下的值差異這麼大,一個在高地址內存空間,而另外一個在低地址內存空間?在執行main函數以前,運行時已經在棧上進行了入棧出棧的操做了,地址-0x10(%rbp)-0x18(%rbp)的值是上一次入棧時寫入的數據,由於沒有進行初始化,因此還保留了上一次操做的值(我的臆測)。

相關文章
相關標籤/搜索