c++編程基礎(2)

作爲數學出身的人,沒有計算機基礎,入門來說是需要時間的,目前來看還是沒有充分利用我的數學知識

現在想整理c編程系列,供自己學習

我們在學任何語言的目的無非是做出我們想要的東西,完成自己的項目,而對於初學者來說,首選面臨的是一個簡單的helloworld程序如何跑起來。本文主要介紹下c程序是如何運行

一,先來個籠統的說法:程序開始執行時,系統爲程序創建一個進程,此時操作系統調用C/C++運行期啓動函數,該函數負責對C/C++運行期庫進行初始化,並保證已經聲明瞭的任何全局對象和靜態對象能夠在代碼執行之前正確的創建;接着系統調用進入點函數(控制檯程序爲main函數),並在main函數裏執行一系列操作;main函數執行完後,從main函數返回(通常main的返回值是int型, 正確返回0. 如果main的返回值爲void或者無, 某些編譯器會給出警告, 此時main的返回值通常是0),啓動函數調用exit()函數,將main的返回值傳遞給它,其中在exit()中會調用ExitProcess()函數,結束進程. 注意,其中exit()原型爲void exit(int status);status爲0表示程序正常退出,非0爲異常退出,但無論怎樣,exit都會刪除進程的內存空間,關閉所有io流,關閉所有句柄、描述字等等資源,也就是說經過exit()後,進程所佔空間和系統資源都已得到正常的釋放。這是exit()與abort的區別.
好了,還是不明白具體的過程,繼續向下
二,總的執行流程

C源程序->編譯預處理->編譯->優化程序->彙編程序->鏈接程序->可執行文件

詳細描述

1、編譯預處理:讀取c源程序,對其中的僞指令(#開頭的)和特殊符號進行處理
                           僞指令: 宏定義指令(#define name tokenStirn#undef)
                           條件編譯指令(#ifdef,#ifndef,#else,#elif,#endif),有選擇的編譯部分代碼
                           頭文件包含指令(#include),系統提供的源程序頭文件在/usr/include下
                           特殊符號:例如LINE標識
經過預編譯處理後的程序是一個沒有宏定義沒有條件編譯沒有特殊符號的文件,這個文件一般格式爲.i,其內容與源代碼有所不同,含義相同


2、經過1的處理後的文件中,將只有常量。如數字、字符串、變量的定義,以及C語言的關鍵字,如main,if,else,for,while,{,},+,-,*,\,等等。預編譯程序所要做得工作就是通過詞法分析和語法分析,在確認所有的指令都符合語法規則之後,將其翻譯成等價的中間代碼表示或彙編代碼。

 
3、優化階段
(1)對中間代碼的優化:刪除公共表達式、循環優化(代碼外提、強度削弱、變換循環控制條件、已知量的合併等)、複寫傳播,以及無用賦值的刪除,等等。

(2)對目標代碼的生成的優化:同機器的硬件結構密切相關,最主要的是考慮是如何充分利用機器的各個硬件寄存器存放的有關變量的值,以減少對於內存的訪問次數。另外,如何根據機器硬件執行指令的特點(如流水線、RISC、CISC、VLIW等)
4.彙編過程
彙編過程實際上指把彙編語言代碼翻譯成目標機器指令的過程。對於被翻譯系統處理的每一個C語言源程序,都將最終經過這一處理而得到相應的目標文件。目標文件中所存放的也就是與源程序等效的目標的機器語言代碼。
目標文件由段組成。通常一個目標文件中至少有兩個段:
代碼段 :該段中所包含的主要是程序的指令。該段一般是可讀和可執行的,但一般卻不可寫。 
數據段 :主要存放程序中要用到的各種全局變量或靜態的數據。一般數據段都是可讀,可寫,可執行的。
UNIX環境下主要有三種類型的目標文件:

(1)可重定位文件  其中包含有適合於其它目標文件鏈接來創建一個可執行的或者共享的目標文件的代碼和數據。

(2)共享的目標文件  這種文件存放了適合於在兩種上下文裏鏈接的代碼和數據。第一種事鏈接程序可把它與其它可重定位文件及共享的目標文件一起處理來創建另一個目標文件;第二種是動態鏈接程序將它與另一個可執行文件及其它的共享目標文件結合到一起,創建一個進程映象。

(3)可執行文件  它包含了一個可以被操作系統創建一個進程來執行之的文件。

彙編程序生成的實際上是第一種類型的目標文件。對於後兩種還需要其他的一些處理方能得到,這個就是鏈接程序的工作了。
5、鏈接程序
一般源文件中的函數可能引用了另一個源文件中定義的某個符號(如變量或者函數調用等);在程序中可能調用了某個庫文件中的函數,等等。所有的這些問題,都需要經鏈接程序的處理方能得以解決。
鏈接程序的主要工作就是將有關的目標文件彼此相連接,也即將在一個文件中引用的符號同該符號在另外一個文件中的定義連接起來,使得所有的這些目標文件成爲一個能夠被操作系統裝入執行的統一整體。
靜態鏈接  在這種鏈接方式下,函數的代碼將從其所在地靜態鏈接庫中被拷貝到最終的可執行程序中。這樣該程序在被執行時這些代碼將被裝入到該進程的虛擬地址空間中。靜態鏈接庫實際上是一個目標文件的集合,其中的每個文件含有庫中的一個或者一組相關函數的代碼。
動態鏈接  在此種方式下,函數的代碼被放到稱作是動態鏈接庫或共享對象的某個目標文件中。鏈接程序此時所作的只是在最終的可執行程序中記錄下共享對象的名字以及其它少量的登記信息。在此可執行文件被執行時,動態鏈接庫的全部內容將被映射到運行時相應進程的虛地址空間。動態鏈接程序將根據可執行程序中記錄的信息找到相應的函數代碼。
6、可執行文件

 看上邊的過程,應該大體上了解了,但是我們知道程序還是依託於硬件跑起來的,在嵌入式系統中,程序最終是要放置在內存中運行的,程序的幾個段,最終會轉化爲內存中的幾個區域。

三,這裏探究下c語言可執行程序的內存佈局(可以pmap指令查看)

一個程序本質上是由BSS段,data段,代碼段組成
1.BSS段:BSS段(bss segment)通常是指用來存放程序中未初始化的全局變量的一塊內存區域。BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態內存分配。一般在初始化時 BSS 段部分將會清零。BSS 段屬於靜態內存分配,即程序一開始就將其清零了。
比如,在C語言之類的程序編譯完成之後,已初始化的全局變量保存在.data 段中,未初始化的全局變量保存在.bss 段中。
2.數據段:數據段(data segment)通常是指用來存放程序中已初始化的全局變量的一塊內存區域。數據段屬於靜態內存分配。data段中的靜態數據區存放的是程序中已初始化的全局變量、靜態變量和常量。
3.代碼段:代碼段(text segment)通常是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定,並且內存區域屬於只讀。在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。
程序編譯後生成的目標文件至少含有這三個段,大體結構如下

其中.text即爲代碼段,爲只讀。.bss段包含程序中未初始化的全局變量和static變量。data段包含三個部分:heap(堆)、stack(棧)和靜態數據區。

(1)•堆(heap):堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
(2)•棧 (stack):棧又稱堆棧, 是用戶存放程序臨時創建的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味着在數據段中存放變 量)。除此以外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,並且待到調用結束後,函數的返回值也會被存放回棧中。由於棧的先進先出特點,所以 棧特別方便用來保存/恢復調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數據的內存區。
stack段存放函數內部的變量、參數和返回地址,其在函數被調用時自動分配,訪問方式就是標準棧中的LIFO方式。(因爲函數的局部變量存放在此,因此其訪問方式應該是棧指針加偏移的方式,否則若通過push、pop操作來訪問相當麻煩)

 下圖對程序段的說明

 

 

四,好了,歇一會,看如下最簡單的C語言Helloword的代碼

#include <stdio.h>

int main()
{
   printf("hello, world\n");
}

上面的代碼我們保存在helloworld.c文件中。其本質實際上是由0、1的比特(位)序列構成的。8位爲一個字節。每個字節對應某個文本字符。不少系統用ASCII來表示文本字符。實際是由一個唯一的同字節大小的整數值來表示每個字符。下面給出helloworld.c的ASCII表示。
#      i         n        c       l       u       d      e     <sp>   <     s     t        d        i       o      .
35   105    110    99    108  117  100   101   32     60  115  116   100   105  111  46
h       >      \n       \n      i        n      t     <sp>   m      a      i       n       (       )      \n     {
104  62     10     10     105  110  116   32    109    97   105  110   40    41   10   123
\n     <sp> <sp> <sp> <sp>   p      r      i       n        t       f      (       "      h       e       l
10    32     32     32     32    112  114  105  110    116  102  40    34   104   101   108
l       o       ,        <sp>  w      o      r       l      d        \       n       "       )       ;       \n      }
108 111    44     32     119  111  114  108  100    92    110  34    41   59      10     125
以此類推,在計算機系統中,任何介質中的數據都是比特序列。把他們區分成不同的數據對象,是通過數據對象的上下文來確定的。

程序編譯
程序的編譯過程如下圖所示,分爲預處理、編譯、彙編、鏈接等幾個階段。

預處理:預處理相當於根據預處理命令組裝成新的C程序,不過常以i爲擴展名。
編譯:    將得到的i文件翻譯成彙編代碼。s文件。
彙編:    將彙編文件翻譯成機器指令,並打包成可重定位目標程序的O文件。該文件是二進制文件,字節編碼是機器指令。
鏈接:    將引用的其他O文件併入到我們程序所在的o文件中,處理得到最終的可執行文件。

硬件組成:
從下圖中看出一個典型的系統由總線、Cpu、I/O設備、主存等構成。

CPU: Central Processing Unit,  ALU: Arithmetic/Logic Unit,  PC: Program counter,  USB: Universal Serial Bus.

程序執行
我們已經討論了可執行文件產生的過程。接下來討論哈可執行文件執行的過程。從上面途中的彩色線條可以清晰的看到這個過程,我們簡單的把它分爲6步。
1.shell程序執行指令,等待用戶輸入,這裏我們輸入“hello”。
2.shell程序將字符逐一讀到寄存器中
3.再從寄存器取出放到主存中
4.當我們敲入回車時,shell程序得知輸入結束,將hello目標文件的代碼和數據拷貝到主存,從而加載hello文件數據包括最終被輸出的字符串“hello,world\n”.利用了DMA訪問技術,數據可不經CPU直接到主存
5.執行主程序中的機器語言指令,將“hello,world\n”串的字節從主存拷貝到寄存器堆。
6.從寄存器中把文件拷貝到顯示設備。

五,總結

上邊是通過網上看別人博客總結,發散思維:上邊只是簡單的一個程序在一臺機子上運行,編譯的可執行文件,爲什麼會在其他的機子上可以運行?多個程序怎麼在一臺機子上運行?如果一個程序足夠大,怎麼在多臺機子上分佈式編譯。

這裏涉及到多線程的東東,後續會研究

 

 



 

發佈了35 篇原創文章 · 獲贊 7 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章