一.軟硬件基本知識linux
1.在計算機多如牛毛的硬件部件中最重要的三個是:中央處理器(CPU)、內存和I/O芯片。下圖爲現代計算機的硬件結構框架程序員
PCI bridge被稱爲北橋,是爲了讓內存等設備可以跟上CPU的頻率。ISA bridge爲南橋,讓低速設備能夠鏈接到北橋上。編程
2.計算機軟件的體系結構:windows
應用程序調用運行庫提供的應用程序編程接口,而運行庫調用操做系統提供的系統調用接口。數組
3.目前全部的操做系統對CPU的分配方式都是以搶佔式進行的,每一個程序會以進程的方式運行,每一個進程有本身獨立的地址,每一個進程根據本身優先級的高低都有機會獲得CPU資源,但當CPU運行超過超過一段時間後,就會讓給其它等待的進程。若是操做系統分配給每一個進程的時間都很短,那麼CPU會頻繁的切換任務,從而形成多任務同時運行的假象。數據結構
4.硬件驅動來搞定硬件操做的繁瑣細節,一般由硬件廠商開發,一般應遵照操做系統的提供的接口跟框架。硬件驅動能夠看作是操做系統的一部分,它與操做系統內核一塊兒運行在特權級。多線程
5.硬盤基本知識:一個硬盤每每有多個盤片,一個盤片有兩面,每面按照同心圓劃分爲多個磁道,每一個磁道劃分爲若干扇區,一個扇區通常爲512字節。每一個扇區有一個邏輯編號併發
6.程序與內存:app
地址空間能夠分爲兩種:虛擬地址空間和物理地址空間,物理地址空間是在計算機中實實在在存在的、惟一的內存地址,虛擬空間是指虛擬的、想象出的地址,每一個進程都有本身獨立的虛擬地址空間,這是程序隔離的方法。框架
(1)最開始,人們採用分段的方法將一段程序所需大小的虛擬空間映射到物理空間,一個字節對應一個字節的嚴格映射,例如:
這樣雖然隔離了程序,但內存使用效率過低。
(2)分頁的方法後來被髮明,其原理是將地址空間人爲的劃分爲頁(page,大小由硬件或操做系統決定,大部分爲4k),下面舉一個簡單例子:
設1頁的大小爲1KB,設兩個程序的虛擬空間地址有8KB,即8個虛擬頁:
假設這是一個32位的電腦,即擁有2^32個物理尋址能力(4G),但假設目前只有6KB的內存(6個物理頁可用),當咱們將進程裏虛擬空間按也劃分,經常使用的代碼或數據頁裝到內存,不經常使用的裝到磁盤(磁盤頁中),須要時再取出來。上圖中的process1的VP0/1/7倍映射到了物理頁PP0/2/3,VP2/3卻存儲在了磁盤的DP0/1(磁盤頁)中,而其餘的注入VP4等可能尚未被用到過。
不一樣的進程可用將本身的虛擬頁映射到同一物理頁,實現了內存的重用,當進程須要使用DP1的數據或代碼時,操做系統會復負責將其從磁盤中調取出來到內存,併爲其與VP3創建映射關係。
下面是虛擬存儲的實現方式:
7.線程:基本組成包括線程ID、當前指令指針(PC)、寄存器集合和棧堆。
(1)從C/C++的角度來說數據在線程中是否私有的關係以下:
單個處理器多線程是一種模擬出來的狀態,多個處理器中,當線程數量小於處理器個數時,是真正的併發運行,當線程數超過處理器個數時,會存在線程調度,這時線程也是一種搶佔式的方式佔有處理器一段時間(時間片)後釋放、等待。
注:搶佔的含義就在於運行完指定的時間後會強制釋放CPU資源。
(2)線程的三種狀態,及其切換:
(3)優先級,通常狀況下,線程都是有優先級的,這個能夠由開發者設定,同時操做系統也會爲線程設定優先級,IO密集型線程的優先級大於CPU密集型的線程。IO密集型線程會頻繁進入等待狀態,不耗CPU。
另外長時間得不到執行的線程也會被提升優先級。
(4)線程模型:大多數操做系統都在內核中對線程進行了支持(內核線程),可是在用戶開發的應用程序中的線程(用戶態線程)並不必定對應一個內核線程。用戶態線程與內核線程的對應關係有三種模型:
一對1、一對多、多對多。通常操做系統API建立的都是一對一線程,如windows的createTread();
2、編譯與連接
1.gcc編譯過程分解:
(1)預編譯的過程包括:展開宏,展開#include引用的h文件(遞歸進行的),處理條件預編譯指令:#if、#ifdef...,刪除全部註釋,添加行號和文件標識,保留全部#pragma指令。
(2)編譯:一系列詞法分析、語法分析、語義分析和優化後生成彙編代碼文件。
(3)彙編:彙編器將彙編語言轉化爲機器能夠執行的機器指令(彙編後即是目標代碼,存在目標文件中)。
(4)連接:將獨立編譯的源代碼模塊組裝起來,即將模塊之間相互引用的地方處理好。
2.編譯:掃描(詞法分析)->語法分析->語義分析->源代碼優化->代碼生成->目標代碼優化
3.連接:
不一樣的模塊(即不一樣的文件)之間編譯是相互獨立的,當文件A調用了文件B中定義的全局變量n時,在編譯A的時候,並不知道n的具體地址(前面說的虛擬地址),所以用0代替。當B編譯完成後,知道了n的具體地址,連接開始進行時,在A中調用n的地方將n的地址替換回去,這個過程叫作重地位。
庫就是一些目標文件的包,最基本的即是系統的運行時庫,它是支持函數運行的基本函數集合,咱們本身寫的程序每每被編譯成目標文件後,都要與運行時庫連接後運行。
三.目標文件
1.目標文件就是通過編譯後,但未進行連接的那些中間文件(windows下的.obj和linux下的.o,又叫可重定位文件),它的格式和最終的可執行文件格式(windows下的exe和linux下的ELF可執行文件)採用同一種格式。同時動態連接庫(windows下的.dll和linux下的.so)和靜態連接庫(windows下的.lib和linux下的.a)文件都按照可執行文件格式存儲。靜態連接庫稍有不一樣,它將多個目標文件捆綁在一塊兒造成一個文件並加上一些索引。
2.目標文件組成:
源代碼編譯後代碼存放在代碼段(名字爲.code或.text),數據存儲在數據段(.data),還有隻讀數據段(.rodata)存放常量用的,以下面程序中的字符常量「%d\n」,這些段也是要按page對齊的,一個簡單程序編譯後結果如圖:
目標文件(或可執行文件等)的file header描述了整個文件的屬性:包括文件是否可執行、是靜態鏈接、是動態連接?,入口地址(若是是可執行文件),目標硬件,目標操做系統等信息。還包括一個段表,用於描述文件中各個段的數組,描述了各個段的偏移位置以及屬性。初始化的全局變量和局部靜態變量存儲在數據段,但未初始化的全局變量和局部靜態變量存儲在一個叫「.bss」的段裏,因爲未初始化的變量在程序中都默認爲0,因此在.data裏都存放一些0是沒有必要的撒。在程序運行時這些變量是要佔內存的,但在文件中,咱們只記錄全部未初始化的全局變量和局部靜態變量所需空間的總和,.bss段至關因而位置的預留,並無內容,在文件中不佔據空間。bss是不佔用.exe文件空間的,其內容由操做系統初始化(清零),好比int a[100],在可執行文件中沒有記錄100個0,而只是記錄了a符號和a所用內存的大小,程序開始運行後,纔會在內存中申請這麼大的地方。bss段的大小存儲在段表裏。
將數據與指令(函數)分開存儲,便於分開裝載,便於同一程序多個副本同時運行時指令的共享,但數據的獨立,以節省內存。
3.ELF可執行文件格式(linux下的可執行文件就是這個格式,windows下是PE,他們都是COFF的變種,因此很相似)。
(1)文件頭(ELF header)包含:ELF魔數(確認文件類型,ELF的16進制型)、文件機器字節長度、數據存儲方式、版本、運行平臺、ABI版本、ELF重定位類型(可重定位文件,即編譯後造成的目標文件(.o),可執行文件,共享目標文件)、硬件平臺、硬件平臺版本、入口地址、程序入口和長度等。
(2)段表(header section table),描述ELF各段的信息(段名、段的長度、在文件中的偏移、讀寫權限及其餘屬性),編譯器、連接器和裝載器都是依靠段表來定位和訪問段的屬性的。其結果以下圖:
段表的字段包括(依次爲:名字、類型、段的標誌位(是否可寫?可執行?須要分配空間?)、地址、偏移、大小、段的連接信息(sh_link/sh_info)。。。):
(3)重定位表:.rel.text,專門記錄須要重定位的位置,若是.data中有數據須要重定位,重定位表爲.rel.data
4.符號表(段名:.symtab):連接的關鍵,尤爲是全局變量和函數的符號名
表示一個符號的結構體(Elf32_Sym):
5.強符號與弱符號
(1)對於C/C++來講,編譯器默認函數以及初始化了的全局函數爲強符號,未初始化的全局變量爲弱符號。位於不一樣文件的兩個強符號名字相同,連接時會報錯。GCC中_attribute_((week))能夠定義任何一個強符號爲弱符號:
(6)強引用與弱引用
連接時,若是沒有找到該符號,會報符號未定義錯誤,這就是一種強引用,相反,不會報錯則爲弱引用,對於未定義的弱引用,連接器默認它爲0.
四.靜態鏈接
1.靜態連接就是將幾個輸入目標文件加工合併成一個輸出目標文件,那麼輸出的目標文件中的空間是如何分配給幾個輸入目標文件的呢?
首先,須要解釋一下這裏的分配地址與空間,這裏的地址與空間有兩層含義:一.輸出到可執行文件的中的空間;二.裝載後的虛擬地址中的虛擬地址空間。對於包含實際數據的段如.text和.data等,他們在文件和虛擬空間中都要分配空間,所以他們在這二者中都存在。而對於.bbs這樣的段來講,分配的意義僅限於虛擬地址空間的分配,由於它在文件中並無內容。事實上,咱們這裏說到的空間分配只關注於虛擬地址空間分配(連接時才分配),這個關係到連接器關於地址計算的步驟。
有2種分配方案:
(1)按序疊加,當輸入的文件過可能是,會產生不少零散的段,因爲段要頁對齊的緣由,還會產生大量空間浪費。
(2)類似段合併,如今連接器基本都有采用這種策略,這種策略鏈接步驟分爲兩步:
第一步:空間與地址分配。獲取全部文件全部段的屬性和長度,獲取全部符號表並統一輩子成全局符號表,計算合併後各個段的長度與位置,並創建映射關係。
第二步:符號解析與重定位(連接的關鍵)。使用上步的信息,獲取文件中的數據與重定位信息,並進行符號解析與重定位、調整代碼中的地址。
當一個目標文件聲明或調用了其它文件裏的變量或函數時,編譯時賦予一些如0x00000000或0xFFFFFFFC來代替,連接的時候,將全部的目標文件合併,重定位便會將真正的地址去替換。重定位信息包含在重定位表(段)裏。
2.C++語言的特有問題(關於連接器的)
(1)重複代碼消除。C++因爲支持模板、外部內聯、虛函數表等特性,會產生大量的重複代碼,如模板,他或許會在不一樣的編譯單元被實例化。重複代碼會形成空間浪費、地址易出錯、指令運行效率低等。
目前主流的作法是:每一個模板的實例都存在單獨的段裏(如:.temp.add<int>與temp.add<float>),連接的時候判斷需不須要將相同代碼合併。
目前一些連接器還支持函數級別的連接,在庫文件很大,但只用其中一兩個函數的時候很實用,其原理是丟棄了用不到的函數,能夠有效減少可執行文件的大小,但減慢了編譯與連接的速度。(也就是在exe中只加載lib裏用到的函數)
3.ABI(application binary interface)指的是符號修飾標準、變量內存佈局、函數調用方式等跟可執行代碼二進制兼容性相關的內容。其影響因素包括:硬件、語言、編譯器、連接器、操做系統等。
因爲ABI差別的問題,讓不一樣編譯器產生的結果很難連接到一塊兒。 C++最爲人詬病的即是這種兼容性問題。
4.連接過程控制的方法有:(1)命令行的方式(如linux下使用Id命令時加 -o,-e等)(2)編譯指令存儲在目標文件的特定段裏(如windows採用的PE/COFF裏的.drectve段)(3)使用連接控制腳本
五.windows COFF/PE
1.windows引入了PE格式的可執行文件(是COFF的一種擴展),其實與ELF同樣也是來源與COFF,所以PE與ELF格式很是類似,在windows中目標文件默認爲COFF,可執行文件爲PE。PE文件在裝載的時候是被直接映射到虛擬空間中運行,它是虛擬空間的映像,因此PE可執行文件也被稱爲映像文件。
同時,PE也是基於段的,一個PE文件至少要包含代碼段:.code,同時在程序中也能夠自定義段名,如:
#pragma data_seg(".FOO")
int global -0;
#pragma data_seg(".data")
表示將全局變量global放在.FOO裏,而後再切換回.data.
如下爲COFF格式:
跟前面講的ELF文件同樣,段表仍然記錄的是各個段的信息,如:段名、物理地址、虛擬地址、原始數據大小、段在文件中的位置、標誌位等等。
2.COFF中的大部分段都與ELF文件類似,惟有兩個是獨特的:
(1)連接指示信息(.drectiva)包含了編譯器想傳遞給連接器的指令。好比說告訴連接器要使用哪一個庫。
(2)調試信息。
3.PE文件結構
它與COFF相比,它的開頭不是COFF的都文件而是DOS MZ可執行文件頭和樁代碼(很大部分是歷史遺留問題,爲了兼容DOS);而COFF原來的頭文件被擴展爲PE頭文件:
六.可執行文件的裝載與進程
1.進程的虛擬地址空間。32位CPU下,程序的虛擬空間不能超過4GB,由於32位CPU只能使用32位指針,其尋址範圍爲0-4GB。可是從硬件層面來講,原先的32位地址線只能訪問4GB物理內存,但Intel公司將地址線拓展爲36位,並修改了頁映射的方式能夠訪問更多物理內存(可達64G),這種地址擴展方式叫作PAE。
2.因爲一般狀況下程序所需內存大於物理內存,所以靜態裝載確定不合適,根據程序的局部性原理,咱們只須要將經常使用的部分裝入內存便可,即動態裝載。有兩種動態裝載的方法:
(1)覆蓋載入,在虛擬內存沒發明前普遍使用,現已被淘汰。這種方式內,分割程序的工做是程序員完成的,暈...
(2)頁映射,內存被劃分爲頁,程序的地址空間也被劃分爲頁。
3.進程建立的過程:
(1)建立獨立的虛擬地址空間。並非真正的建立一塊實在的空間,而是建立映射函數所需的相應的數據結構,好比頁目錄。
(2)讀取可執行文件頭,創建虛擬空間與可執行文件的映射關係。其關係如圖所示:
很明顯,這種映射關係只是保存在操做系統內部的一個數據結構。
須要注意的是這裏只是可執行文件和虛擬頁之間的映射關係,虛擬頁與物理頁之間的映射會在頁錯誤時發生。
(3).將CPU指令寄存器設置成可執行文件入口,啓動運行。
4.頁錯誤
上面的步驟執行完後,並無任何程序裝載到內存,只是創建了虛擬地址空間與可執行文件的映射,並指定了程序的入口,當CPU開始執行這個入口指令的時候,發現是一個空白頁,這被認爲是一個頁錯誤,發生頁錯誤後,CPU將控制權交給操做系統,操做系統會查找裝載時創建的那個數據結構,找到應該被加載進來的程序虛擬地址空間,並分配物理頁面,創建虛擬頁與物理頁的映射,進而將程序指令加載進來。隨着進程的執行,會有頁錯誤不斷髮生。
5.進程虛擬存儲的分佈
(1)因爲映射都是以頁爲單位的,所以爲了不空間地址被過多浪費,能夠將相同權限的段合併成一個段進行映射。段的權限主要有三種:可讀可執行(如:.text等)、可讀可寫段(如.data,.BBS等)、只讀(只讀數據段等)。合併的段被稱爲segment,他們在一塊兒映射以後,在虛擬空間中只有一個地址呦。因此根據section(段)和segment劃分可執行文件,能夠被稱爲不一樣的視圖(view),從section來看就是elf的連接視圖,從segment來看就是elf的執行視圖。
(2)堆和棧。一個進程中的棧和堆都有對應的虛擬空間地址。C語言中的malloc()是從堆裏分配的。一個進程包含如下幾種VMA(虛擬空間地址),討論segment,基本也就指這幾種VMA:
程序的運行是根據虛擬空間(可執行文件的一種映射)來進行的。
七.動態連接
1.靜態連接對計算機內存和磁盤空間的浪費嚴重,例如,linux中一個程序所需的C語言靜態庫至少1M,那麼若是機器中運行着100個這樣的程序,就要浪費近100M內存空間。若是磁盤中有兩千個這樣的程序文件,得佔2G磁盤。
也就是說同一個目標文件被兩個程序都靜態連接時,它會在內存和磁盤中出現兩個副本,這就是一種浪費。
另外,若是對任意一個靜態連接庫進行修改,那麼整個程序就要所有從新連接,這不利於程序的發佈,所以那種萬年不會變的庫採用動態連接。
2.動態連接的基本思想是將程序的模塊拆分紅相對獨立的模塊(主程序(可執行文件)、動態連接庫都是模塊),當程序運行時纔將他們連接在一塊兒造成完整的程序。而不像靜態連接一個將全部須要的模塊都連接成一個完整的可執行文件。當某一個動態連接庫被加載到內存中後,若是其餘程序在運行過程當中也須要加載它,那麼直接連接已經在內存中存在的動態連接庫就能夠了,這樣一個動態連接庫老是在內存中只有一個副本。動態連接讓程序開發更加靈活。
3.動態連接的過程:程序編程成目標文件->靜態連接造成可執行文件,同時將程序中須要動態連接的符號標記一下->將可執行文件與動態連接庫(linux中叫動態共享對象dso)裝載連接運行。
這裏須要注意的是,靜態連接(連接器)過程當中,動態連接庫仍然會被做爲輸入文件之一,由於連接器將會利用它的符號表將程序中須要動態連接的地方作標記。
4.動態連接庫的虛擬地址空間沒法預先固定,舉個例子:某個程序,模塊A(多是動態連接庫或可執行文件)的地址爲0x1000-0x2000,模塊B的地址爲0x2000-0x3000,另一我的寫了一個程序,要調用調用A裏的函數,但不調用B,這時對於改程序而言,0x2000-0x3000這塊地址是空閒的,因而程序將這塊地址分配給一個開發的新的模塊C,若是其餘程序要調用B和C時會發生嚴重的目標地址衝突。
爲了解決這個問題,程序中動態連接對象的虛擬地址肯定應在動態連接庫裝載完成後,在進行重定位。當動態連接庫裝載地址肯定後,系統會對目標程序中全部標記了動態連接對象的地方進行重定位。例如,當動態連接庫被裝載到進程虛擬空間的0x10000000地址後,假設其foo()函數位於0x100000100處,這時系統將遍歷目標程序的重定位表,將全部調動foo的地方所有替換爲0x100000100。這種重定位原理與靜態連接的重定位同樣,靜態連接是:程序編譯時不知道的指令地址在鏈接時重定位,動態連接是:連接後仍不知道的指令地址在裝載時重定位。
我以爲上述重定位的過程只是把原來在靜態連接時的重定位延後到裝載時進行了,其他並無什麼區別。
這種方法有一個問題就是不一樣的進程之間將不能共享指令部分,緣由是重定位時有指令會被修改,好比,模塊A調用了模塊B,模塊B重定位後須要修改A的指令,其餘進程調用A時,顯然不能共享前面已經裝載的A。這樣喪失了其節省內存的優點。
5.爲了解決上述問題,使用一種地址無關代碼的技術。
咱們根據各類類型的地址引用方式來分別介紹代碼無關技術:
(1)模塊內部函數的調用、跳轉等。這個最簡單,模塊內部的函數調用處於同一模塊,相對位置固定,能夠直接利用相對地址調用。所以這自己就是一種地址無關的代碼。
(2)模塊內數據訪問。雖然代碼段佔若干頁、數據段佔若干頁,但他們的頁之間的相對位置也是固定的。這也是一種地址無關代碼。
(3)模塊間的數據訪問。因爲要訪問的數據被定義在另外的模塊,只能在裝載的時候再肯定,爲了使指令部分的地址與代碼無關,將與地址有關的代碼所有放到數據段裏面。這樣數據段(包含一部分代碼)就是地址相關的咯,而代碼段爲地址無關的。ELF文件會在數據段裏面創建一些指向須要調用的外部變量的指針數組,稱爲全局偏移表(GOT),當代碼須要該全局變量(或定義在其它模塊的靜態變量)時能夠經過GOT間接引用。每一個變量的地址在GOT中佔4字節,裝載完成的時候,連接器會找到這些變量的地址,將它們填充到GOT。因爲GOT放在數據段,因此即便它在模塊裝載時須要修改也不受影響,由於每一個進程中,被調模塊的數據段老是有獨立的副本。
(4)模塊間的調用、跳轉等。方法與上面相似,只不過GOT中保存的是函數的位置。
地址無關的共享對象叫作PIC,linux能夠在編譯時指定參數 -fPIC來實現。同理還能夠實現地址無關的可執行文件PIE。
6.動態連接的過程
(1)動態連接器的自舉。首先動態連接器自己也是一個共享對象,首要工做是先將本身重定位。
(2)裝載共享對象。a.動態連接器將可執行文件和連接器自己的符號表合併成全局符號表。b。動態連接器尋找可執行文件依賴的共享對象,並將它們的名字放入到裝載集合中。c.連接器開始從裝載集合中取出一個名字,打開該文件,將其代碼段和數據段映射到進程的內存空間中來。新共享對象的符號表會與全局符號表合併。d.判斷如若該共享對象還依賴於其餘共享對象則對其進行上述循環。
(3)重定位與初始化。
以上三個過程完成後,動態連接器就會將進程的控制權轉交給入口程序。
八.windows下的動態連接
dll文件和exe文件其實是一個概念。dll與so相比更加註重模塊化設計,使得模塊之間能夠鬆散耦合、重用和升級,Windows上大量的軟件都是經過升級dll進行完善,微軟常常將這些升級補丁累計到一個軟件升級包,如office、VS、甚至windows操做系統等。
九.程序的內存佈局
1.通常來說,應用程序在內存中有如下「默認」區域:
(1)棧,用於維護函數調用上下文。一般在程序的最高地址處分配,一般有數兆字節。windows默認一個線程是1M的棧
(2)堆,用來容納應用程序動態分配的內存區域(malloc,new),堆通常比棧大不少幾十甚至數百兆。
(3)可執行文件映像,存儲可執行文件在內存中的映像(注意,採用的是頁映射),包括代碼段、數據段等。
(4)保留區,並非指一個單一的區域,而是內存中受到保護而禁止訪問的內存區域總稱。
(5)動態連接庫映射區,用於映射裝載的動態連接庫。
下圖是一個典型的linux內存分佈圖:圖中的箭頭表明大小可變區域的尺寸增加方向。
2.棧。一般保存了一個函數調用所須要的維護信息,包括:
(1)函數的返回地址與參數。
(2)臨時變量。包括函數的非靜態局部變量,以及編譯器生成的臨時變量。
(3)保存的上下文。包括函數調用先後須要保持不變的寄存器。
每個函數都有一塊棧區,咱們稱之爲棧幀。
下面說說函數p調用函數q時的具體狀況。當執行call q(y1)時,會爲函數q建立一個新的棧幀,具體過程是:先保存上一幀的地址,若是有返回值的話爲返回值分配存儲空間,而後保存返回地址。而後爲y1分配空間並把它初始化爲調用q時給的參數。接着分配另外一個參數的空間y2,這個參數用於在函數內部計算。
3.堆。申請的堆在詢空間中是連續的,但在物理空間中就不必定是連續的了。
十.運行庫
1.C/C++程序運行步驟:
(1)操做系統建立進行,將控制權交給入口函數,注意這裏入口函數指的並非main函數,每每是運行庫中的某個入口函數。
(2)入口函數對程序的運行環境和運行庫進行初始化,包括堆、線程、I/O、全局變量構造等等。
(3)入口函數在初始化完成後調用main函數,執行程序主體。
(4)main函數執行結束後,返回入口函數,進行各類清理工做。