1. 程式人生 > >《程式是怎樣跑起來的》(摘要)

《程式是怎樣跑起來的》(摘要)

最近讀了日本作者矢澤久雄寫的《程式是怎麼跑起來的》,也在網上找了人家的書評,書評為計算機組成原理學習的不是很好的同學是一個不錯的學習機會,自己也感覺受益匪淺,希望對大家的學習有所幫助。

Q:電腦的CPU中包含哪些部分?各自的作用有哪些?
A:CPU包含暫存器,控制器,時鐘和運算器四種主要的結構。

 - 控制器負責將記憶體上的指令、資料等讀入到暫存器,並根據運算的結果控制整個計算機;
 - 暫存器用來暫存資料、指令等處理物件,一般CPU包含20~100個不同的暫存器;
 - 時鐘負責CPU開始計時的時鐘訊號;
 - 運算器負責運算從記憶體讀入暫存器的資料


從程式設計師的角度來說,CPU可以看作暫存器的集合。CPU中包含不同種類的暫存器,各自有不同的功能。


Q:一個典型的C語言原始碼在電腦中執行的基本流程是怎樣的?
A:C語言寫成的原始碼是高階語言程式,但是CPU執行的程式碼是本地機器語言,因此C的原始碼並不能立即執行。實際上,一個C的原始碼需要經過編譯、和連結生成.exe的可執行檔案之後,電腦會將.exe檔案的副本複製到記憶體中再執行。

Q:記憶體內部結構如何?記憶體的資料存取都有哪些資料結構?
A:記憶體是計算機的主儲存器,通過晶片與計算機相連,主要負責儲存指令和資料,CPU通過基址暫存器和變址暫存器讀取和寫入記憶體中的資料。記憶體由連續的長度為8bit(1個位元組)的基本元素構成,程式啟動之後CPU的控制暫存器根據時鐘訊號從記憶體中讀取指令和資料。


存取記憶體的資料結構包括陣列、棧、堆、佇列、連結串列和二叉樹。我們可以通過指標直接訪問和改變對應記憶體地址中的變數的數值。

- 陣列是多個同樣型別的資料在記憶體中連續的排列的形式,可以通過陣列的索引訪問陣列元素;
- 棧可以不通過指定地址和索引對陣列元素進行讀寫。棧由棧底、棧頂描述,一般用來臨時儲存運算過程中的資料、連線在計算機裝置上或者輸入輸出的資料;
- 佇列與棧相似,棧的元素是FILO,但是佇列是FIFO,佇列一般用環形緩衝區實現;
- 連結串列與陣列不同,它在記憶體中不是連續儲存的,每個元素都有一個直接後繼,像串珠一樣將每個元素串聯起來,最大優勢是增減元素方便快捷;
- 二叉樹中除了最終的子節點之外,每個元素都有兩個後繼結點,有序二叉樹使得搜尋變得更有效


Q:資料和程式是如何儲存在計算機中的?
A:程式和資料是儲存在計算機的硬碟中的,但是程式執行需要將機器語言的程式載入到記憶體,因為CPU的程式計數器指定記憶體地址才能讀出程式內容。記憶體和磁碟因為自身特點的差異,它們之間具有緊密的聯絡。

- 磁碟快取。由於磁碟的讀取速度較慢,為了加快程式的執行,將磁碟中的部分資料載入到記憶體中快取起來,之後在訪問同一個資料的時候就直接從記憶體中讀取資料,這樣的機制叫**磁碟快取**;
- 虛擬記憶體。**虛擬記憶體**剛好與之相反,在執行比較大的程式或者記憶體資源比較緊張可以將部分磁碟當作**假想的記憶體**來用。實現虛擬記憶體機制需要在磁碟為記憶體預留空間,並在程式執行時與記憶體中的內容進行置換(swap),window中提過**分頁式虛擬記憶體機制**。一般虛擬記憶體的大小與記憶體相當或者是記憶體的兩倍。

Q:什麼是動態連結和靜態連結?二者有何不同?
A:DLL(Dynamic link libary)是在程式執行時候動態載入的檔案。

所謂動態連結,就是把一些經常會共用的程式碼(靜態連結的OBJ程式庫)製作成DLL檔,當執行檔呼叫到DLL檔內的函式時,Windows作業系統才會把DLL檔載入記憶體內,DLL檔本身的結構就是可執行檔,當程式需求函式才進行連結。透過動態連結方式,記憶體浪費的情形將可大幅降低。

簡單來說,已經編譯成組合語言的程式檔案,在進一步連結時如果直接將庫檔案連結進exe可執行檔案,則該連結檔案就是靜態庫,如果僅僅在程式執行時才進行連結稱為動態連結,連結的目標檔案就是動態連結庫(windows中為dll檔案)。需要說明的是,在連結之後,exe檔案中包含了靜態連結庫的所有內容,所以會比較大,而動態連結庫相對輕巧,並且**動態連結庫可以在被多個同時執行的程式所共有,並且保證記憶體中只有一個dll檔案中呼叫函式的副本**,這樣就節省了程式執行的空間。實際上window作業系統的大部分API目標檔案是動態連結庫,動態連結庫一般由匯入庫匯入,匯入庫中並不存在目標函式的實體,僅僅儲存目標函式所在的動態連結庫的名稱及路徑。

關於動態連結和靜態連結的詳細介紹請參考博文[C++靜態庫與動態庫](http://www.cnblogs.com/skynet/p/3372855.html "C++靜態庫與動態庫")。

**Q:一個C語言源程式是如何變成可執行檔案(exe)的?又是如何在作業系統中執行的?**
**A**:這是個比較大的問題,作者在書中舉了個C語言的例子。大體來說,C的源程式需要通過編譯器編譯成組合語言(asm檔案),進一步連結需要的庫檔案(dll檔案)生成可執行檔案(exe檔案),最後點選exe將可執行檔案匯入記憶體執行程式。以Sample.c檔案為例

    #include <stdio.h>
    #include <windows.h>

    char *title = "messgae box";
    double average(double a, double b)
    {
        return (a + b)/2.0; 
    }

    int WINAPI WinMain(HINSTANCE h, HINSTANCE d, LPSTR s, int m)
    {
        double ave;
        char buff[80];
        ave = average(123,456);

        sprintf(buff, "average value is %f", ave);

        MessageBox(NULL, buff, title, MB_OK);

        return 0;
    }

1. **編譯**該檔案,在原始檔目錄上執行命令`bcc32 -W -c Sample.c`,生成sample.obj目標檔案;
2. **連結**需要的庫檔案,執行命令`ilink32 -Tpe -c -x -aa c0w32.obj Sample.obj, Sample.exe,, import32.lib cw32.lib`

需要說明的是,c0w32.obj檔案是與所有程式起始位置相結合的處理內容,稱為程式的**啟動**。在源程式中,我們**呼叫**了系統函式sprintf和messagebox,因此,需要將這兩個函式對應的庫函式(其中的內容與exe檔案相同,都是原生代碼)**連結**進來,告訴連結器去哪裡找這兩個函式對應的原生代碼。

sprintf的原生代碼在cwlib32.lib中,編譯之後會將它的目標函式合成到exe檔案中,稱為**靜態連結**;而messagebox的原生代碼在庫檔案user32.dll裡,使用import32.dll是為了告訴聯結器“messagebox在庫檔案user32.dll中,以及user32.dll在哪裡”,所以import32.dll稱為匯入庫。程式執行時,執行從DLL檔案調出的MessageBox()函式這一資訊就會和exe檔案結合,稱為**動態連結**。

**Q:可執行檔案包含哪些內容?它載入到記憶體中是什麼樣子?**
**A**:可執行檔案中包含了源程式的變數和函式的虛擬地址,在載入到記憶體之後需要必要的資訊將虛擬地址轉換成實際地址,轉換需要的資訊就在exe檔案開始的部分,稱為再配置資訊。exe檔案被載入到記憶體之後,就將這些虛擬記憶體轉換成實際記憶體,程式執行中會生成棧和堆,因此在記憶體中的樣子如下圖所示

[![3gXgTTNBST7Ua27e9SaE2B5d.jpg](https://s20.postimg.org/ttpdsl5rx/3g_Xg_TTNBST7_Ua27e9_Sa_E2_B5d.jpg)](https://postimg.org/image/bqwb1d9x5/)


**Q:c,o,a,lib,obj,dll這些檔案分別是什麼?他們之間是什麼關係?**
**A**:c是C語言的原始檔,如博文[Linux的.a、.so和.o檔案](http://blog.csdn.net/chlele0105/article/details/23691147 "Linux的.a、.so和.o檔案") 中所述
> lib,dll,exe都算是最終的目標檔案,是最終產物。而c/c++屬於原始碼。原始碼和最終目標檔案中過渡的就是中間程式碼obj,實際上之所以需要中間程式碼,是你不可能一次得到目標檔案。比如說一個exe需要很多的cpp檔案生成。而編譯器一次只能編譯一個cpp檔案。這樣編譯器編譯好一個cpp以後會將其編譯成obj,當所有必須要的cpp都編譯成obj以後,再統一link成所需要的exe,應該說缺少任意一個obj都會導致exe的連結失敗。而 .o,是Linux目標檔案,相當於windows中的.obj檔案,.so檔案為共享庫,是shared object,用於動態連線的,相當於windows下的dll,.a為靜態庫,是好多個.o合在一起,用於靜態連結


**Q:什麼是_BSS段和_DATA段?全域性變數和區域性變數在程式執行時有何不同?**
**A**:這是組合語言的概念,編譯器將高階語言源程式轉換成彙編檔案(.asm檔案),有如下的原始檔sample2.c

    int AddNum(int a, int b)
    {
        return a + b;
    }

    void MyFun()
    {
        int c;
        c = AddNum(123,456);
    }

經過編譯之後的彙編檔案(軟體環境win10,gcc編譯器)內容如下:
```
    .file "sample.c"
    .text
    .globl _AddNum
    .def _AddNum; .scl 2; .type 32; .endef
_AddNum:
    pushl %ebp
    movl %esp, %ebp
    movl 8(%ebp), %edx
    movl 12(%ebp), %eax
    addl %edx, %eax
    popl %ebp
    ret
    .globl _MyFun
    .def _MyFun; .scl 2; .type 32; .endef
_MyFun:
    pushl %ebp
    movl %esp, %ebp
    subl $24, %esp
    movl $456, 4(%esp)
    movl $123, (%esp)
    call _AddNum
    movl %eax, -4(%ebp)
    leave
    ret
    .ident "GCC: (tdm-1) 4.9.2"
```

彙編程式最接近機器語言,而且其與C語言一一對應,所以通過彙編檔案就可以瞭解程式執行的大體情況。從上面的彙編檔案,可以看到如下的結果

1. 暫存器esp指向棧頂元素地址,每個元素佔據4個位元組的資料;
2. 在每個函式開始的時候,都要將暫存器ebp的資料壓入棧中進行保護;
3. 上述程式中隱藏的一個關鍵步驟是在第21行,call AddNum時,計算機已經將MyFun函式的下一個指令的地址壓入棧中,在呼叫完AddNum時(第12行),返回函式Myfun時候會自動將棧中的返回指令的地址出棧交給CPU的程式計數器,這樣就可以實現在呼叫函式之後仍然返回原來的呼叫的地方;
4. 函式的入參被儲存在棧中,返回值被儲存在暫存器裡。

```c
int a;
int b;
float fl;

int c = 9;
int d = 10;
int e = 11;
int f = 12;

void MyFun(void)
{
    int a1,b1,c1;
    float fl1;
    a1 = 1;
    b1 = -1;
    fl1 = -99.34;
    c1 = -87;

    a1 = a;
    b1 = b;
    fl1 = fl;
    c1 = c;
}
```
以上的C原始碼轉換成組合語言是

```
    .file "sample2.c"
    .comm _a, 4, 2
    .comm _b, 4, 2
    .comm _fl, 4, 2
    .globl _c
    .data
    .align 4
_c:
    .long 9
    .globl _d
    .align 4
_d:
    .long 10
    .globl _e
    .align 4
_e:
    .long 11
    .globl _f
    .align 4
_f:
    .long 12
    .text
    .globl _MyFun
    .def _MyFun; .scl 2; .type 32; .endef
_MyFun:
    pushl %ebp
    movl %esp, %ebp
    subl $16, %esp
    movl $1, -4(%ebp)
    movl $-1, -8(%ebp)
    movl LC0, %eax
    movl %eax, -12(%ebp)
    movl $-87, -16(%ebp)
    movl _a, %eax
    movl %eax, -4(%ebp)
    movl _b, %eax
    movl %eax, -8(%ebp)
    movl _fl, %eax
    movl %eax, -12(%ebp)
    movl _c, %eax
    movl %eax, -16(%ebp)
    leave
    ret
    .section .rdata,"dr"
    .align 4
LC0:
    .long -1027166700
    .ident "GCC: (tdm-1) 4.9.2"

```

從中可以看出全域性變數儲存在.comm和.globl段,區域性變數儲存在暫存器中,因此在程式執行的整個過程中,全域性變數可以隨時訪問,但是區域性變數卻會在用過之後消失。

關於windows的彙編的內容可進一步參考文章[彙編與逆向分析](http://www.mouseos.com/assembly/index.html "彙編與逆向分析")