天天看點

《程式員的自我修養—連結、裝載與庫》讀書筆記(更新中)第一章第二章第三章

文章目錄

  • 第一章
  • 第二章
  • 第三章

第一章

  1. 接口:
  • 應用程式使用的是作業系統應用程式程式設計接口,由運作庫提供,例如Linux下Glibc的POSIX的API,Windows提供的WIndows API
  • 運作庫使用OS提供的系統調用接口,在實作層面由軟體中斷提供,如Linux使用0x80号中斷作為系統調用接口,Windows使用0x2E号中斷作為系統調用接口
  • OS核心與硬體層的接口為硬體規格
  1. 作業系統的兩個主要功能是提供接口和管理資源
  2. 現代OS是一種多任務系統,OS管理所有硬體資源,本身運作在一個受硬體保護的級别。所有應用程式都以程序的形式運作在一個比OS更低的級别,CPU由OS統一配置設定
  3. 硬體驅動與OS一起運作在特權級,但是于核心有一定獨立性。例如在UNIX中,硬體被抽象為普通的檔案;Windows中,圖形硬體被抽象為GDI,聲音和多媒體裝置被抽象為DirectX對象,磁盤被抽象為檔案系統
  4. 分段和分頁
  5. 線程 Thread
  • 一個标準的線程由線程ID、目前指令指針PC、寄存器集合和堆棧構成

    -

    《程式員的自我修養—連結、裝載與庫》讀書筆記(更新中)第一章第二章第三章
  • 線程可以通路程序在記憶體裡的所有資料,包括其他線程的堆棧
  • 單處理器運作多線程時,需要線程排程。一個線程可以有三種狀态
    • 運作
    • 就緒
    • 等待

      運作狀态的線程擁有一個時間片,用盡後進入就緒狀态,若未用盡就在等待某事件,則進入的等待狀态。每當一個線程離開運作狀态,排程系統就會選擇一個其他的就緒線程繼續執行。線程擁有自己的優先級,某些OS會根據線程優先級來優先排程高優先級線程進入運作狀态。通常,I/O密集型線程比CPU密集型線程優先級高。一個長事件得不到排程的線程稱為餓死 Starvation

  • 搶占與不可搶占:

    在可搶占系統中,線程用盡時間片被強制剝奪執行權,進入就緒狀态這一過程即搶占 Preemption

    在不可搶占系統中,線程主動放棄執行隻有兩種情況

    • 線程等待I/O
    • 線程主動放棄時間片
  1. Linux的多線程

    Linux将所有執行實體(程序和線程)都稱之為任務 Task,每個任務都類似一個單線程程序,具有記憶體空間,執行實體,檔案資源等。但Linux下不同任務可以共享記憶體空間,多個共享同一記憶體空間的多個任務就構成了一個程序。

    Linux中管理任務的方式如下:

  • fork:産生一個和目前程序完全一樣的新程序,老程序和新程序一塊從fork傳回,但老程序傳回新任務的pid,新任務傳回0
  • exec:使新的可執行映像覆寫目前的可執行映像,fork新任務之後,可以用exec執行新的可執行檔案
  • clone:建立子程序并從指定位置開始執行
  1. 線程安全
  • 原子操作
  • 同步與鎖:
    • 信号量
    • 互斥量
    • 臨界區
    • 條件變量:對于條件變量,線程有兩種操作:等待和喚醒。一個條件變量可以被多個線程等待,喚醒會使所有等待在該條件變量上的線程全部喚醒
    • 讀寫鎖:有兩種擷取方式,共享Shared和獨占Exclusive,在共享鎖已被擷取的狀态下,其他線程依然可以以獨占方式獲得鎖;但是想以獨占方式獲得鎖,隻能等待鎖被所有線程釋放
讀寫鎖狀态 以共享方式擷取 以獨占方式擷取
自由 成功 成功
共享 成功 等待
獨占 等待 等待
  • 可重入 Reentrant

    重入:一個函數被重入,表示該函數沒有執行完成,由于外部因素或内部調用,又一次進入該函數。函數被重入的情況:

    • 多個線程同僚執行這個函數
    • 函數遞歸調用自身

      一個函數是可重入的代表這個函數被重入後不會産生任何不良後果,可重入函數包含以下幾個特點:

    • 不适用任何局部靜态或全局非const變量
    • 不傳回任何局部靜态或全局的非const變量
    • 僅依賴于調用方提供的參數
    • 不依賴任何單個資源的鎖
    • 不調用任何不可重入的函數

      可重入函數可以在多線程環境下使用

  • 三種線程模型
    • 核心線程由多核CPU或排程算法實作并發,而使用者線程不一定與核心線程有一對一關系,例如可能三個使用者線程對于核心來說隻有一個線程
    • 一對一模型:

      一個使用者态線程對應一個核心态線程。此模型下使用者态線程可以做到真正的并發。Linux中利用clone産生的線程就是一對一線程

    • 多對一模型

      多個使用者态線程被映射到同一個核心線程,線程間的切換由使用者态代碼進行,其中一個使用者态線程阻塞,其他線程都無法運作,因為核心線程也被阻塞了

    • 多對多模型

      多個使用者态線程被映射少數但不止一個核心态線程上,其中一個使用者态線程的阻塞不會影響其他線程

第二章

  1. 編譯的4個步驟
  • 預處理
    • 删除所有#define,并且展開所有宏定義
    • 處理所有條件預編譯指令,諸如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”
    • 處理所有#include預編譯指令,将頭檔案插入到預編譯指令的位置(遞歸進行,也會處理頭檔案裡的頭檔案)
    • 删除所有注釋 “//”、“”
    • 添加行号,便于編譯器調試、報錯
    • 保留所有#pragma編譯器品質你個
  • 編譯
    • 将預處理完的檔案進行一系列詞法分析、文法分析、語義分析、優化,生成相應的彙編代碼
  • 彙編
    • 将彙編代碼轉為機器碼
  • 連結
  1. 編譯器是一個将進階語言翻譯成機器語言的一個工具,從源代碼到目标代碼,可以看作經曆了6步:掃描、文法分析、語義分析、源代碼優化、代碼生成核目标代碼優化
  • 詞法分析:源代碼輸入掃描器,運用一種類似有限狀态機的算法将源代碼字元序列分割為一系列記号 Token,大緻分為幾類:關鍵字、辨別符、字面量(數字、字元串 etc.)、特殊符号(加号、等号 etc.)
  • 文法分析:對Token進行文法分析,産生文法樹,采用上下文無關文法的分析手段。文法樹是以表達式為節點的樹
  • 語義分析:由語義分析器完成,編譯器能分析的語義是靜态語義,是編譯期可以确定的語義,與之對應的是動态語義,即運作期才能确定的語義。靜态語義通常包括聲明和類型比對、類型轉換
  • 中間語言生成:源碼級優化器在源碼級别進行優化,将文法樹轉換為中間代碼
  • 目标代碼生成和優化,使用代碼生成器和目标代碼優化器
  1. 子產品之間的互相通信有兩種方式:子產品間的函數調用和子產品間的變量通路
  2. 靜态連結

    連結的過程主要包括位址和空間配置設定、符号決議、重定位等

    基本的靜态連結:目标檔案.o或.obj檔案和庫一起連結形成最終的可執行檔案,最常見的庫是運作時庫,庫其實是一組目标檔案的包

第三章

  1. 可執行檔案、目标檔案、靜态連結庫、動态連結庫都可以按照可執行檔案格式存儲,均為Linux的**ELF (Executable Linkable Format)**格式檔案
ELF檔案類型 說明 執行個體
可重定位檔案 Relocatable File 靜态連結庫、目标檔案 .o(Linux)、.obj(Windows)
可執行檔案 Executable File 無擴充名 可直接執行 bash檔案、.exe(Windows)
共享目标檔案 Shared Object File 一種是連結器可以使用這種檔案連結其他檔案 另一種是動态連結器可以将幾個這種目标檔案和可執行檔案結合,作為程序映像的一部分來運作 .so(Linux)、DLL(Windows)
核心轉儲檔案 Core Dump File 程序意外終止時,系統可以将該程序位址空間的内容和其他資訊轉儲到這種檔案 core dump(Linux)
  1. 目标檔案

    目标檔案中包含編譯後的機器指令代碼、資料和連結所需的資訊,一般這些資訊以節 Section或段 Segment存儲。

    源代碼編譯後的機器指令碼被放在代碼段 Code Segment,全局變量和局部靜态變量被放在資料段 Data Segment。

    指令和資料分段存儲的優勢:

  • 程式裝載後,資料和指令被映射到兩個虛拟記憶體區域,資料區域是可讀寫的,指令區域是隻讀的,防止指令被改寫
  • CPU的緩存 Cache體系被設計為資料緩存和指令緩存分離,程式的指令和資料分離可以提高緩存命中率
  • OS中運作多個程式的副本時,指令隻需要儲存一份
  1. 段表 Section Header Table

    儲存段的基本屬性:段名、段長、偏移、讀寫權限、其他屬性

  2. 重定位表 Relocation Table

    連結器在處理目标檔案時,某些部位必須被重定位,這些資訊記錄在重定位表裡

  3. 字元串表
  4. 連結與符号

    例如,目标函數B要用到目标函數A中的函數foo,則A 定義 Define了foo,B 引用 Reference了foo,變量同理。連結中,變量和函數統稱為符号 Symbol,他們的名字成為符号名 Symbol Name

    連結過程需要基于符号來完成,每個目标檔案都有一個符号表,記錄了目标檔案所用到的所有符号,每個定義的符号都有一個對應的符号值 Symbol Value,對于符号,符号值就是他們的位址,符号可以分為以下幾類:

  • 定義在本目标檔案的全局符号,可以被其他檔案引用
  • 在本目标檔案中引用的全局符号,未定義在本目标檔案中:外部符号 Extern Symbol
  • 段名
  • 局部符号,僅在編譯單元内部可見
  • 行号
  1. 符号修飾與函數簽名

    Unix下的C語言規定,C語言源代碼檔案中所有全局變量和函數編譯後,對應的符号名前面會加上下劃線“_”,以差別其他語言編寫的檔案,但是無法解決同一種語言多個子產品間的沖突,C++針對此引入了命名空間 Namespace

    C++的符号修飾:符号修飾 Name Decoration、符号改編 Name Mangling

    C++允許函數靜态重載,也支援命名空間,即在不同名程空間可以擁有多個同名符号,所引入了函數簽名 Function Signature,函數簽名包含一個函數的資訊:函數名、參數類型、所在的類和名稱空間。編譯器與連結器在處理符号時,使用名稱修飾方法,使得每個函數簽名對應一個修飾後名程 Decorated Name。是以C++源代碼編譯後的目标檔案使用的都是修飾後名稱,編譯器和連結器對于不同函數簽名的函數,無論函數名是否相同,都看作不同的函數。

  2. extern “C”

    C++為了相容C語言,引入了一個關鍵字“extern C”,在其後的大括号裡的代碼會被當做C語言處理,在這裡,C++名稱修飾機制将不起作用

    例如:

extern C{
	int func(int);
	int var;
}
           

聲明了一個函數func,定義了一個變量var。在Visual C++下,會将這兩個符号名前加上下劃線"_"進行修飾,Linux的GCC下編譯器不會修飾。

一個頭檔案中聲明了C語言的函數和全局變量,這個頭檔案可能會被C語言代碼和C++代碼包含,例如C庫string.h中的memset函數,原型如下:

void *memset(void *,int,size_t)

若不加任何處理,C程式包含string.h頭檔案時,可以正确處理,C++則不然,必須用extern C聲明memset,但因為C不支援extern C文法,則需要使用宏“__cplusplus”,C++編譯器在編譯C++時會預設定義這個宏,來表征這是一個C++檔案,是以我們可以用條件宏來判斷是否C++代碼:

#ifdef __cplusplus
extern C{
#end if
void *memset(void *,int,size_t)
#ifdef __cplusplus
}
#endif
           
  1. 弱符号與強符号

    例如在目标檔案A、B中都定義全局整型變量,則連結器連結A、B時會報錯,這種符号即強符号。C/C++編譯器預設函數和初始化了的全局變量為強符号,未初始化的全局變量為若符号 。連結器的處理規則如下:

  • 不允許強符号多次定義
  • 若一個符号在某個目标檔案中為強符号,其他目标檔案中為若符号,則選擇強符号
  • 若一個符号在所有目标檔案中均為弱符号,選擇占用空間最大的一個

強引用與弱引用: