學號:sa×××310 姓名:××濤
環境:Opensuse 12.2 gcc4.7.1
1.gdb常用調試命令
要用gdb調試的話,編譯命令需要添加-g參數,例如
gcc -g main.c -o main
b linenum 在第 linenum行打斷點
l 顯示原始碼;
Ctrl-d 退出gdb
where 顯示當前程式運行位置
print /d $eax 十進位地方式列印$eax 值,/x是十六進位,/t是二進位
c 執行到下一個斷點
n 下一行
layout split 把當前Terminal分割成兩半,上面顯示源碼及彙編,下面可以輸入調試命令,效果如下:
2.Example.c程式分析
程式碼:
#include <stdio.h>int g(int x){return x+3;}int f(int x){return g(x);}int main(void){printf("Hello\n");return f(8)+1;}
將原始碼編譯為二進位檔案又需要經過以下四個步驟:預先處理(cpp) → 編譯(gcc或g++) → 彙編(as) → 連結(ld)
;括弧中表示每個階段所使用的程式,它們分別屬於 GCC 和 Binutils 軟體包。
用gcc的編譯參數和產生的對應檔案。
2.1先行編譯
gcc -E Example.c -o Example.cpp
產生的cpp檔案內容如下:
.........//a lot of extern statementextern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));# 910 "/usr/include/stdio.h" 3 4extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));# 940 "/usr/include/stdio.h" 3 4# 2 "Example.c" 2int g(int x){ return x+3;}int f(int x){ return g(x);}int main(void){ return f(8)+1;}
主要代碼基本沒有變化,添加了很多extern聲明。
分析
先行編譯的主要作用如下:
●將源檔案中以”include”格式包含的檔案複製到編譯的源檔案中。
●用實際值替換用“#define”定義的字串。
●根據“#if”後面的條件決定需要編譯的代碼。
在該階段,編譯器將C原始碼中的包含的標頭檔stdio.h編譯進來,產生擴充的c程式。當對一個源檔案進行編譯時間, 系統將自動引用預先處理程式對來源程式中的預先處理部分作處理, 處理完畢自動進入對來源程式的編譯。
2.2編譯
執行編譯的結果是得到彙編代碼。
gcc -S Example.c -o Example.s
產生.s檔案內容如下:
.file"Example.c".text.globlg.typeg, @functiong:.LFB0:.cfi_startprocpushl%ebp ;ebp寄存器內容壓棧.cfi_def_cfa_offset 8.cfi_offset 5, -8movl%esp, %ebp ;esp值賦給ebp,設定函數的棧基址。.cfi_def_cfa_register 5movl8(%ebp), %eax ;將ebp+8所指向記憶體的內容存至eaxaddl$3, %eax ;將3與eax中的數值相加,結果存至eax中popl%ebp ;ebp中的內容出棧.cfi_restore 5.cfi_def_cfa 4, 4ret.cfi_endproc.LFE0:.sizeg, .-g.globlf.typef, @functionf:.LFB1:.cfi_startprocpushl%ebp ;ebp寄存器內容壓棧.cfi_def_cfa_offset 8.cfi_offset 5, -8movl%esp, %ebp ;esp值賦給ebp,設定函數的棧基址。.cfi_def_cfa_register 5subl$4, %esp ;esp下移動四個單位movl8(%ebp), %eax ;將ebp+8所指向記憶體的內容存至eaxmovl%eax, (%esp) ;將eax存至esp所指記憶體中callg ;調用g函數leave ;將ebp值賦給esp,pop先前棧內的上級函數棧的基地址給ebp,恢複原棧基址 .cfi_restore 5.cfi_def_cfa 4, 4ret ;函數返回,回到上級調用.cfi_endproc.LFE1:.sizef, .-f.globlmain.typemain, @functionmain:.LFB2:.cfi_startprocpushl%ebp ;ebp寄存器內容壓棧.cfi_def_cfa_offset 8 .cfi_offset 5, -8movl%esp, %ebp ;esp值賦給ebp,設定函數的棧基址。.cfi_def_cfa_register 5subl$4, %esp ;esp下移動四個單位movl$8, (%esp) ;將8存入esp所指向的記憶體空間 callf ;調用f函數addl$1, %eax ;將1與eax的內容相加leave ;將ebp值賦給esp,pop先前棧內的上級函數棧的基地址給ebp,恢複原棧基址 .cfi_restore 5.cfi_def_cfa 4, 4ret ;函數返回,回到上級調用 .cfi_endproc.LFE2:.sizemain, .-main.ident"GCC: (SUSE Linux) 4.7.1 20120723 [gcc-4_7-branch revision 189773]".section.comment.SUSE.OPTs,"MS",@progbits,1.string"ospwg".section.note.GNU-stack,"",@progbits
分析
第1行為gcc留下的檔案資訊;第2行標識下面一段是程式碼片段,第3、4行表示這是g函數的入口,第5行為入口標號;6~20行為 g 函數體,稍後 分析;21行為 f 函數的程式碼片段的大小;22、23行表示這是 f 函數的入口;24行為入口標識,25到41為 f 函數的彙編實現;42行為f函數的程式碼片段的大小;43、44行表示這是main函數的入口;45行為入口標識,46到62為main函數的彙編實現;63行為main函數的程式碼片段的大小;54到67行為 gcc留下的資訊。
具體程式運行時記憶體的調用情況如:
以.cfi開頭的命令如.cfi_startproc,主要用於作用是出現異常時stack的復原(unwind),而復原的過程是一級級CFA往上回退,直到異常被catch。
這裡不做討論,需要詳細瞭解的點這裡。
每一個函數在開始都會調用到
pushl %ebp ;ebp寄存器內容壓棧,即儲存函數的上級調用函數的棧基地址 movl %esp,%ebp ;esp值賦給ebp,設定函數的棧基址
主要作用是儲存當前程式執行的狀態。
還有兩句在函數調用結束時也會出現:
leave ; 將ebp值賦給esp,pop先前棧內的上級函數棧的基地址給ebp,恢複原棧基址 ret ; 函數返回,回到上級調用
用於在函數執行完後回到執行前的狀態。
還有要注意的是彙編中的push和pop
pop系列指令的格式是:
pop destination
pop指令把棧頂指定長度的資料存放到destination中,並且設定相應的esp的值使它始終指向棧頂位置。
push剛好相反。
pushl %eax 等價於
subl $4 %esp
movl %eax (%esp)
popl %eax 等價於
movl (%esp) %eax
addl %4 %esp
2.3彙編
彙編之後得到的是.o檔案,終端執行命令:
as Example.s -o Example.o
在終端用vim開啟:
vim -b Example.o
用16進位進行查看,在vim中輸入
:%!xxd
結果如下(未完全顯示)
分析
目標檔案就是原始碼編譯後但未進行連結的那些中間檔案,包含有編譯後的機器指令代碼,還包括連結時所需要的一些資訊,比如符號表、調試資訊、字串等。
可以查看目標檔案的資訊,在終端執行
file Example.o
得到:
Example.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
其中的relocatable指出該檔案為ELF中的可重定位檔案類型。
2.4連結
連結後的檔案為可執行檔,在linux中沒有副檔名。
終端執行:
gcc Example.o -o Example
執行Example,終端運行:
./Example
運行結果:
、
分析
用file命令查看Example屬性:
file Example
Example: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.16, BuildID[sha1]=0xffdc8de348d59ce38f1f933e55b7a5c55184ef39, not stripped
其中的executable指出該檔案為ELF中的可執行檔類型。
由於程式沒有任何列印語句,所以程式執行完之後就直接退出了。
3.電腦工作流程-單任務和多任務暫且討論最簡單的電腦,只包含CPU,儲存空間,I/O控制晶片如果一個使用者在同一時間只能運行一個應用程式,則對應的作業系統稱為單任務作業系統,如MS-DOS。如果使用者在同一時間可以運行多個應用程式(每個應用程式被稱作一個任務),則這樣的作業系統被稱為多任務作業系統,如windows 7,Mac OS 。在最早期的單任務電腦中,使用者一次只能運行一個程式,電腦首先從外存中載入程式到記憶體,然後依次執行程式指令,完全執行完畢之後才可以載入、執行下一個程式。由於當時CPU的資源十分珍貴,為了充分利用,在這之後出現了多道程式,當某個程式暫時無需使用CPU時,監控程式就把另外的正在等待CPU資源的程式啟動,使得CPU充分利用。缺點是程式的運行沒有優先順序。在這之後又出現了分時系統,程式運行模式變成了一種協作的模式,即每個程式運行一段時間以後都主動讓出CPU。分時系統繼續發展就到了今天的多任務系統 - 所有的程式都以進程的方式運行在比作業系統許可權更低的層級,每個進程都有自己的獨立空間,CPU由作業系統統一進行分配,每個進程根據進程優先順序的高低都有獲得CPU的機會。多任務的實現主要依靠MMU(Memory Management Unit:記憶體管理單元)。MMU的主要工作就是將程式的虛擬位址(編譯器和連結器計算的)轉換成記憶體的物理地址(硬體電路決定的)。MMU可以通過重定位任務地址而不需要移動在記憶體中的任務。任務的實體記憶體只是簡單的通過啟用與不啟用頁表來實現映射到虛擬記憶體。 4.參考資料程式員的自我修養—連結、裝載與庫
Computer Systems: A Programmer's Perspective 3rd Edith