《Linux C編程:一站式學習》筆記

文|MESeraph

10 | gdb

在不利用gdb調試之前,我們一般通過以下步驟發現bug的原因:通過錯誤現象,假設錯誤原因,再通過插入printf,執行程序並分析結果。
這樣費時費力,而通過gdb可以完全復現錯的現象。(當然如果是多線程的問題,可以會比較複雜一點,因爲線程之間交互過多,打斷點也經常會阻礙線程的正常交互)。

  1. 編譯時加上-g選項便可以生產可調試的可執行文件。
  2. 在提示符下直接按回車鍵表示重複上一條命令。
  3. 把源代碼改名或移到別處再用gdb調試,這樣就列不出源代碼了。
命令 簡寫 含義
help [指令] h 查看幫助
list [數字|函數名] l 從第n行或函數頭開始查看源碼,每次列出10行
quit q 退出gdb調試
start 開始執行調試
next n 單步調試
step s 進入函數內部跟蹤調試
backstrace bt 查看函數調用的棧幀
info locals i locals 查看局部變量值;
frame f 切換棧幀
print p 打印變量值
finish 讓程序執行完當前函數
set var 變量 給變量賦值
dispaly/undisplay 變量 跟蹤/取消跟蹤變量
breakpoint 行數 b 設置斷點,在某行設置斷點,一般是會先使用l顯示行
continue c 繼續執行
i breakpoints i b 查看已經設置的斷點
delete breakpoints [數字] 刪除第n個斷點或全部
disable breakpoints [數字] 暫停第n個斷點或全部
enable breakpoints [數字] 啓用第n個斷點或全部
break n if 條件 滿足條件則執行斷點
run r 重新運行調試程序
x/數字b 變量 打印變量位置的指定數量的單位內存信息
watch array[5] 監視array[5]內存改變
i watchpoints 查看監視點

13 | 計算機中數的表示

  1. 邏輯電路計算兩個bit的加法:
    在這裏插入圖片描述
  2. 多位加法器
    在這裏插入圖片描述
  3. 十進制小數換算成二進制小數:乘2取整,順序排列。
  4. 數的表示法:
  • Sign and Magnitude表示法:把最高位規定爲符號位(Sign Bit),0表示正1表示負,剩下的7位表示絕對值的大小,8個bit表示整數的取值範圍是271271-2^7-1~2^7-1
    用這種表示法進行減法的缺點:計算機做加減運算需要處理很多邏輯:比較符號位、比較絕對值、加法改減法、減法改加法、小數減大數改成大數減小數……這是非常低效率的。還有一個缺點是0的表示不唯一,既可以表示成10000000也可以表示成00000000。
  • 1’s Complement表示法
    十進制9補碼計算理解:
    167-52=167+(-52)=167+(999-52)-1000+1=167+947-1000+1=1114-1000+1=114+1=115
    首先-52要用999-52表示,就是947,這稱爲取9的補碼(9’sComplement);然後把167和947相加,得到114進1;再把高位進的1加到低位上去,得115,本來應該加1000,結果加了1,少加了999,正好把先前取9的補碼多加的999抵消掉了。
    二進制1補碼計算理解:
    00001000-00000100→00001000+(-00000100)→00001000+11111011→00000011進1→高位進的1加到低位上去,結果爲00000100(正負得正的情況)
    1’s Complement表示法缺點:0的表示仍然不唯一,既可以表示成11111111也可以表示成00000000。
  • 2’s Complement表示法
    2’s Complement表示法規定:正數不變,負數先取反碼再加1。
    如果8個bit採用2’s Complement表示法,負數的取值範圍是從10000000到11111111(-128~-1),正數是從00000000到01111111(0~127),也可以根據最高位判斷一個數是正是負,並且0的表示是唯一的,目前絕大多數計算機都採用這種表示法。
    的,目前絕大多數計算機都採用這種表示法。爲什麼稱爲“2的補碼”呢?因爲對一位二進制數b取補碼就是1-b+1=10-b,相當於從2裏面減去b。
    判斷溢出:如果兩個正數相加溢出,結果一定是負數;如果兩個負數相加溢出,結果一定是正數;一正一負相加,無論結果是正是負都不可能溢出。
    在這裏插入圖片描述
    依據上面的情況分析得出結論:在相加過程中最高位產生的進位和次高位產生的進位如果相同則沒有溢出,如果不同則表示有溢出。
    邏輯電路的實現可以把這兩個進位連接到一個異或門,把異或門的輸出連接到溢出標誌位。
  1. 浮點數計算
  • 正規化(Normalize):規定尾數部分的最高位必須是1,也就是說尾數必須以0.1開頭,對指數做相應的調整。由於尾數部分的最高位必須是1,這個1就不必保存了,可以節省出一位來用於提高精度,我們說最高位的1是隱含的(Implied)。
    在這裏插入圖片描述

  • 有時計算順序不同也會導致不同的結果,因爲浮點數計算時,後面的小數可能會被捨去。

14 | 數據類型詳解

一、整型
  1. C標準的Rationale之一:優先考慮效率,而可移植性尚在其次。所以效率和可移植性需要自己作選擇。
  2. C語言與平臺和編譯器是密不可分的,離開了具體的平臺和編譯器討論C語言。
  3. C標準沒有明確規定char是有符號的還是無符號的,但是要求編譯器必須對此做出明確規定,並寫在編譯器的文檔中。
  4. Implementation-defined表示沒有明確規則,但是編譯器必須明確規定。(比如char是有符號還是五符號)
    Unspecified的情況,C標準沒有明確規定按哪種方式處理,編譯器可以自己決定,並且也不必寫在編譯器的文檔中,這樣即便用同一個編譯器的不同版本來編譯也可能得到不同的結果。(比如求知順序)
    Undefined的情況則是完全不確定的,C標準沒規定怎麼處理,編譯器很可能也沒規定,甚至也沒做出錯處理,有很多Undefined的情況編譯器是檢查不出來的,最終會導致運行時錯誤。(比如數組訪問越界)
二、 浮點數
  1. 有的處理器有浮點運算單元(Floating Point Unit,FPU),稱爲硬浮點(Hard-float)實現;有的處理器沒有浮點運算單元,只能做整數運算,需要用整數運算來模擬浮點運算,稱爲軟浮點(Soft-float)實現。
三、類型轉換
  1. 有符號或無符號的char型、short型和Bit-field在進行算術運算之前首先要做Integer Promotion,然後才能參與計算。
        • / % > < >= <= == !=運算符都需要做UsualArithmetic Conversion。
  2. 單目運算符+ - ~只有一個操作數,移位運算符<< >>兩邊的操作數類型不要求一致,這些運算不需要做Usual Arithmetic Conversion,但需要做Integer Promotion.
  3. getchar的返回值是int型。
四、強制類型轉換

在這裏插入圖片描述
一定要注意強制類型轉換,最好是不要出現數值超過轉換目標類型的範圍。

15 | 運算符詳解

一、位運算
  1. C語言中其實並不存在8位整數的位運算,操作數在做位運算之前都至少被提升爲int型了。
  2. 右移運算的規則,如果是負數,則是Implementation-defined。
  3. 由於類型轉換和移位等問題,用有符號數做位運算是很不方便的,所以,建議只對無符號數做位運算,以減少出錯的可能性。
  4. 一個數和自己做異或的結果是0。如果需要一個常數0,x86平臺的編譯器可能會生成這樣的指令:xorI %eax, %eax。不管eax寄存器裏的值原來是多少,做異或運算都能得到0,這條指令比同樣效果的movI $0, %eax指令快,直接對寄存器做位運算比生成一個立即數再傳送到寄存器要快一些。
  5. 從異或的真值表中可以看出,和0做異或保持原值不變,和1做異或得到原值的相反值。得到原值的相反值。可以利用這個特性配合掩碼實現某些位的翻轉。
  6. 如果a1 ^ a2 ^ a3 ^ … ^ an的結果是1,則表示a1、a2、a3…an之中1的個數爲奇數個,否則爲偶數個。校驗碼會用到這個性質。
  7. x ^ x ^ y == y,這個性質可以用來不借助額外的存儲空間交換來兩個變量的值。
a = a^b;
b = b^a;
a = a^b;
  1. RAID(Redundant Array of Independent Disks,獨立磁盤冗餘陣列)實際上就是利用了7、8。
二、其他
  1. size_t就代表unsigned long型。不同平臺的編譯器可能會根據自己平臺的具體情況定義size_t所代表的類型,比如有的平臺定義爲unsigned long型,有的平臺定義爲unsigned long long型,C標準規定size_t這個名字就是爲了隱藏這些細節,使代碼具有可移植性。
  2. 類型名也遵循標識符的命名規則,並且通常加個_t後綴表示Type。

16 | 計算機體系結構基礎

  1. 地址線、數據線和CPU寄存器的位數通常是一致的。
  2. 對於多字節的整數類型,低地址保存的是整數的低位,這稱爲小端(Little Endian)字節序(Byte Order)。x86平臺是小端字節序的,而另外一些平臺規定低地址保存整數的高位,稱爲大端(Big Endian)字節序。
  3. 無論是在CPU外部接總線的設備還是在CPU內部接總線的設備都有各自的地址範圍,都可以像訪問內存一樣訪問,很多體系結構(比如ARM)採用這種方式操作設備,稱爲內存映射I/O(Memory-mapped I/O)。但是x86比較特殊,x86對於設備有獨立的端口地址空間,CPU核需要引出額外的地址線來連接片內設備(和訪問內存所用的地址線不同),訪問設備寄存器時用特殊的in/out指令,而不是和訪問內存用同樣的指令,這種方式稱爲端口I/O(Port I/O)。
    在這裏插入圖片描述
  4. 從CPU的角度來看,訪問設備只有內存映射I/O和端口I/O兩種,要麼像內存一樣訪問,要麼用一種專用的指令訪問。
  5. 訪問設備是相當複雜的,計算機的設備五花八門,各種設備的性能要求都不一樣,有的要求帶寬大,有的要求響應快,有的要求熱插拔,於是出現了各種適應不同要求的設備總線,比如PCI、AGP、USB、1394、SATA等,這些設備總線並不直接和CPU相連,CPU通過內存映射I/O或端口I/O訪問相應的總線控制器,通過總線控制器再去訪問掛在總線上的設備。
  6. 訪問設備還有一點和訪問內存不同。內存只是保存數據而不會產生新的數據,如果CPU不去讀它,它也不需要主動給CPU提供數據,所以內存總是被動地等待被讀或者被寫。而設備往往會自己產生數據,並且需要主動通知CPU來讀這些數據,例如輸入一個字符,用戶希望計算機馬上響應自己的輸入,這就要求鍵盤設備主動通知CPU來讀這個字符並做相應的處理,給用戶響應。這是由中斷(Interrupt)機制實現的,每個設備都有一條中斷線,通過中斷控制器連接到CPU,當設備需要主動通知CPU時就引發一箇中斷信號,CPU正在執行的指令將被打斷,程序計數器會指向某個固定的地址(這個地址由體系結構定義),於是CPU從這個地址開始取指令(或者說跳轉到這個地址),執行中斷服務程序(Interrupt Service Routine,ISR),完成中斷處理之後再返回先前被打斷的地方執行後續指令。
  7. 由於各種設備的操作方法各不相同,每種設備都需要專門的設備驅動程序(Device Driver),一個操作系統爲了支持廣泛的設備就需要有大量的設備驅動程序,事實上Linux內核源代碼中絕大部分是設備驅動程序。設備驅動程序通常是內核裏的一組函數,通過讀寫設備寄存器實現對設備的初始化、讀、寫等操作,有些設備還要提供一箇中斷處理函數供ISR調用。
  8. MMU
    在這裏插入圖片描述
    如果處理器沒有MMU,或者有MMU但沒有啓用,CPU執行單元發出的內存地址將直接傳到芯片引腳上,被內存芯片(以下稱爲物理內存,以便與虛擬內存區分)接收,這稱爲物理地址(Physical Address,PA)。
    如果處理器啓用了MMU,CPU執行單元發出的內存地址將被MMU截獲,從CPU到MMU的地址稱爲虛擬地址(Virtual Address,VA),而MMU將這個地址翻譯成另一個地址發到CPU芯片的外部地址引腳上,也就是將VA映射成PA。
  9. 如果是32位處理器,則內地址總線是32位的,與CPU執行單元相連,而經過MMU轉換之後的外地址總線則不一定是32位的。也就是說,虛擬地址空間和物理地址空間是獨立的,32位處理器的虛擬地址空間是4GB,而物理地址空間既可以大於4GB也可以小於4GB。(注意!注意!注意!)
  10. 物理內存中的頁稱爲物理頁面或者頁幀(Page Frame)。虛擬內存的頁面映射到物理內存的頁幀是通過頁表(Page Table)來描述的,頁表保存在物理內存中,MMU會查找頁表來確定一個VA應該映射到什麼PA。
  11. CPU每次執行訪問內存的指令都會自動引發MMU做查表和地址轉換操作,地址轉換操作由硬件自動完成,不需要用指令控制MMU去做。
  12. MMU提供內存保護機制,操作系統可以在頁表中設置每個內存頁面的訪問權限,有些頁面不允許訪問,有些頁面只有在CPU處於特權模式時才允許訪問,有些頁面在用戶模式和特權模式都可以訪問,訪問權限又分爲可讀、可寫和可執行三種。這樣設定好之後,當CPU要訪問一個VA時,MMU會檢查CPU當前處於用戶模式還是特權模式,訪問內存的目的是讀數據、寫數據還是取指令,如果和操作系統設定的頁面權限相符,就允許訪問,把它轉換成PA,否則不允許訪問,產生一個異常(Exception)。
  13. 異常的處理過程和中斷類似,不同的是中斷由外部設備產生而異常由CPU內部產生,中斷產生的原因和CPU當前執行的指令無關,而異常的產生就是由於CPU當前執行的指令出了問題。
  14. 段錯誤的產生:
  • 用戶程序要訪問的一個VA,經MMU檢查無權訪問。
  • MMU產生一個異常,CPU從用戶模式切換到特權模式,跳轉到內核代碼中執行異常服務程序。
  • 內核把這個異常解釋爲段錯誤,終止引發異常的進程。

17 | x86彙編程序基礎

  1. 鏈接主要有兩個作用:
  • 一是修改目標文件中的信息,對地址做重定位。
  • 二是把多個目標文件合併成一個可執行文件
    所以彙編器編譯及其指令後,還需要鏈接。
  1. 彙編指令:as
  2. 鏈接指令:ld
一、彙編語法
  1. .開頭的名稱並不是指令的助記符,不會被翻譯成機器指令,而是給彙編器一些特殊指示,稱爲彙編指示(AssemblerDirective)或僞操作(Pseudo-operation),由於它不是真正的指令所以加個“僞”字。
  2. .section指示把代碼劃分成若干個段(Section),程序被操作系統加載執行時,每個段被加載到不同的地址,操作系統對不同的頁面設置不同的讀、寫、執行權限。
    .data段保存程序的數據,是可讀可寫的,相當於C程序的全局變量。
    .text段保存代碼,是隻讀和可執行的
  3. _start是一個符號(Symbol),符號在彙編程序中代表一個地址,可以用在指令中,彙編程序經過彙編器的處理之後,所有的符號都被替換成它所代表的地址值。
  4. .gIobI告訴彙編器,_start這個符號要被鏈接器用到,所以要在目標文件的符號表中標記它是一個全局符號。
  5. _start就像C程序的main函數一樣特殊,是整個程序的入口,鏈接器在鏈接時會查找目標文件中的_start符號代表的地址,把它設置爲整個程序的入口地址,所以每個彙編程序都要提供一個_start符號並且用.gIobI聲明。如果一個符號沒有用.gIobI聲明,就表示這個符號不會被鏈接器用到。
  6. 立即數前面要加$,寄存器名前面要加%,以便跟符號名區分開。
  7. int指令稱爲軟中斷指令。
  8. 內核提供了很多系統服務供用戶程序使用,但這些系統服務不能像庫函數(比如printf)那樣調用,因爲在執行用戶程序時CPU處於用戶模式,不能直接調用內核函數,所以需要通過系統調用切換CPU模式,經由異常處理程序進入內核,用戶程序只能通過寄存器傳幾個參數,之後就要按內核設計好的代碼路線走,而不能任由用戶程序隨心所欲地調用內核函數,這樣可以保證系統服務被安全地調用。
  9. eax和ebx是傳遞給系統調用的兩個參數。eax的值是系統調用號,Linux的各種系統調用都是由int $0x80指令引發的,內核需要通過eax判斷用戶需要哪個系統調用,_exit的系統調用號是1。ebx的值是傳給_exit的參數,表示退出狀態。
  10. x86彙編一直存在兩種不同的語法,在intel的官方文檔中使用intel語法, Windows也使用intel語法,而UNIX平臺的彙編器一直使用AT&T語法。
  11. data_items類似於C語言中的數組名。
  12. .long指示聲明佔32位的數
    .byte聲明佔8位的數
    .ascii,聲明取值爲相應字符的ASCII碼的字符。
二、x86的寄存器
  1. x86的通用寄存器有eax、ebx、ecx、edx、edi、esi。這些寄存器在大多數指令中是可以任意選用的,但也有一些指令規定只能用其中某個寄存器做某種用途。
  2. x86的特殊寄存器有ebp、esp、eip、efIags。eip是程序計數器,
    efIags保存着計算過程中產生的標誌位,其中包括進位標誌、溢出標誌、零標誌和負數標誌,在intel的手冊中這幾個標誌位分別稱爲CF、OF、ZF、SF。ebp和esp用於維護函數調用的棧幀。
三、尋址方式
  1. 通用內存尋址指令格式:ADDRESS_OR_OFFSET(%BASE_OR_OFFSET,%INDEX,MULTIPLIER)
  2. 直接尋址、變址尋址、間接尋址、基址尋址、立即尋址、寄存器尋址。
四、ELF文件
  1. 各種UNIX系統的可執行文件都採用ELF格式,它有以下三種不同的類型:
  • 可重定位的目標文件(Relocatable,或者Object File)
  • 可執行文件(Executable)
  • 共享庫(Shared Object,或者Shared Library)
  1. 編譯、鏈接、運行過程:
  • 彙編器讀取這個文本文件並將其轉換成目標文件max.o,目標文件由若干個Section組成,我們在彙編程序中聲明的.section會成爲目標文件中的Section,此外匯編器還會自動添加一些Section(比如符號表)。
  • 然後鏈接器把目標文件中的Section合併成幾個Segment,生成可執行文件max。
  • 最後加載器(Loader)根據可執行文件中的Segment信息加載運行這個程序。ELF格式提供了兩種不同的視角,鏈接器把ELF文件看成是Section的集合,而加載器把ELF文件看成是Segment的集合。
  1. 有些Section只對鏈接器有意義,在運行時用不到,也不需要加載到內存,那麼就不屬於任何Segment。
  2. 使用readeIf工具查看目標文件內容。
  3. 使用hexdump工具查看目標文件字節內容。
  4. C語言的全局變量如果在代碼中沒有初始化,就會在程序加載時用0初始化。這種數據屬於.bss段。
  5. 在ELF文件中.data段需要佔用一部分空間保存初始值,而.bss段則不需要。
  6. .reI.text告訴鏈接器指令中的哪些地方需要做重定位。
  7. .symtab是符號表。
  8. 使用objdump工具反彙編。
  9. 兩個Segment必須加載到內存中兩個不同的頁面,因爲MMU的權限保護機制是以頁爲單位的,一個頁面只能設置一種權限。
  10. strip命令去除可執行文件中的符號信息。不要對目標文件和共享庫使用strip命令,因爲鏈接器需要利用目標文件和共享庫中的符號信息來做鏈接。

21 | Makefile編程基礎

一、語法規則
  1. Makefile由一組規則(Rule)組成,每條規則的格式如下所示:
target ... : prerequistites ...
	command1
	command2
	...

目標和條件之間的關係是:欲更新目標,必須先更新它的所有條件;所有條件中只要有一個條件被更新了,目標也必須隨之被更新。
所謂“更新”就是執行一遍規則中的命令列表,命令列表中的每條命令必須以一個Tab開頭,注意不能用空格代替這個Tab。
對於Makefile中的每個以Tab開頭的命令,make會啓動一個Shell進程去執行它。
如下例子:

collectsvr: collectsvr.o getconf.o httpclt.o 
	gcc collectsvr.o getconf.o httpclt.o -lcurl -o collectsvr
collectsvr.o: collectsvr.c include/httpclt.h include/getconf.h 
	gcc -c collectsvr.c
httpclt.o: httpclt.c include/httpclt.h
	gcc -c httpclt.c
getconf.o: getconf.c include/getconf.h
	gcc -c getconf.c
  1. 嘗試更新Makefile中第一條規則的目標main,第一條規則的目標稱爲缺省目標,只要缺省目標更新了就算完成任務了,其他工作都是爲這個目標而做的。

  2. 通常Makefile都會有一個clean規則,用於清除編譯過程中產生的二進制文件,保留源文件:

clean:
	@echo "cleaning project"
	-rm collectsvr *.o
	@echo "cleaning completed"

在make的命令行中可以指定一個或多個目標,比如指定了目標clean,則執行Makefile中更新目標clean的規則,如果在make的命令行中不指定任何目標,則更新Makefile中第一條規則的目標(缺省目標)。
我們輸入make clean便可以指定clean目標。
如果make執行的命令前面加了@字符,則不顯示命令本身而只顯示它的輸出結果;
但如果命令前面加了-字符(Hyphen),即使這條命令出錯,make也會繼續執行後續命令。
如果存在clean這個文件,clean目標也不依賴於任何條件,make就認爲它不需要更新了。所以這個時候我們需要一條僞目標命令來告訴make指令,這條目標不是真正的目標:.PHONY: clean

  1. 約定俗成的目標名字有:
  • all,執行主要的編譯工作,通常用作缺省目標。
  • install,執行編譯後的安裝工作,把可執行文件、配置文件、文檔等分別複製到不同的安裝目錄。
  • clean,刪除編譯生成的二進制文件。
  • distclean,不僅刪除編譯生成的二進制文件,也刪除其他的生成文件,比如內核源代碼make menuconfig配置之後生成的.config文件,一些文檔源文件(比如本書的Docbook源文件)經過make之後會轉換生成HTML或PDF文件,執行make distclean應該清除所有的生成文件,只留下源文件。
二、隱含規則和模式規則
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章