C語言 ---- 第四章 函數庫

第一節 系統服務

  • 寫一個程序,實現在屏幕上顯示一串字符的功能,從代碼開始執行到字符顯示在屏幕上,這中間都發生了什麼?
  • 補充知識:I/O設備
    • I/O設備分爲三部分:設備、設備控制器、設備驅動程序
      • 設備:顯示器、鍵盤、鼠標、硬盤、打印機、麥克風、···
      • 設備控制器:顯卡、聲卡、網卡、硬盤控制器、···
      • 設備驅動程序:每一種設備都有相應的設備驅動程序
    • 要想在屏幕上顯示一個字符,顯示器顯卡及其相應的設備驅動程序,三者缺一不可
  • 從代碼開始執行到字符顯示在屏幕上,這中間都發生了什麼?顯示器、顯卡及其相應的設備驅動程序,它們分別處於整個顯示過程的什麼位置?
    • 程序向操作系統發送服務請求
    • 操作系統把服務請求翻譯給設備驅動程序
    • 設備驅動程序根據服務請求與設備控制器進行交涉
    • 設備控制器最終控制設備完成我們所請求的服務
  • 在屏幕上顯示一個字符聽起來很簡單,但具體實現過程相當複雜,但是,在整個實現的過程中,和程序有直接關係的只有操作系統。程序只要向操作系統發送服務請求即可,剩下的事情就和程序沒有關係了,就好像是操作系統屏蔽了硬件的所有細節,我們不需要了解硬件的任何相關信息,操作系統背後的東西與我們無關。
  • 操作系統爲程序提供了在屏幕上輸出字符的服務,對於我們來說,只需要按照操作系統所規定的方法向操作系統發送服務請求,就可以實現這個功能。除此之外,操作系統還提供了其他服務,將操作系統提供的這些服務簡稱爲系統服務

第二節 系統調用

  • 程序具體應該如何向操作系統請求系統服務?

    • 我們需要按照操作系統所規定的方法來請求系統服務,不同的操作系統對應着不同的請求方法,但是它們的原理是相同的。

    • 以Linux操作系統爲例,探究在Linux操作系統中如何請求系統服務:

      • C語言並不具備請求系統服務的能力,需要通過在C程序中嵌入彙編代碼來實現系統服務的請求。

      • int main(){
            char A[] = {"Hello\n"};
            __asm__(
            	"mov rax, 1 \n\t"
                "mov rdi, 1 \n\t"
                "syscall \n\t"
                :
                :"S"(A),"d"(6)
            );
            
            return 0;
        }
      • __asm__();是彙編代碼的一個標識,用來區分彙編代碼和C代碼,如果不按照這個格式寫,C實現就無法判斷這段代碼是彙編代碼還是C代碼,就會按照C語言的語法對代碼進行解析,這就必然會導致編譯失敗。

      • " \n\t":和C語言中的;作用相同,用於代碼和代碼之間的區分,表示mov rax, 1是一條完整的代碼。

      • "mov rax, 1 \n\t":rax寄存器中存儲的值爲系統服務號,將rax寄存器的值設置爲1,也就是要求rax寄存器提供1號系統服務,即寫文件和設備服務。

      • "mov rdi, 1 \n\t":rdi寄存器決定寫哪個文件和設備,將rdi寄存器的值設置爲1,也就是要求rdi寄存器寫控制檯。

      • "syscall \n\t":向操作系統請求系統服務,操作系統會根據上述兩行代碼的要求來提供相應的系統服務。

      • "S"(A):S表示rsi寄存器,用於存儲要顯示字符(A)的內存地址

      • "d"(6):d表示rdx寄存器,用於存儲要顯示的字符(“Hello\n”)數量

      • ::寄存器和C語言的變量進行交換數據的指令前面要加上冒號,以示和其他指令的區別

  • 總結:

    • 操作系統爲我們提供了很多服務(比如在控制檯顯示字符的服務),我們把操作系統提供的服務簡稱爲“系統服務”。
    • 操作系統爲我們提供了調用系統服務的“入口點”,即syscall,我們可以通過syscall這個入口點來調用系統服務,把用來調用系統服務的入口點簡稱爲“系統調用”。(syscall 就是 system call的簡寫)
    • 通過系統調用獲取系統服務,需要用匯編代碼來實現。
  • 根據以上總結,可以得出以下結論:

    • C語言並不具備在屏幕上顯示字符的能力,顯示字符是一件很複雜的事情;
    • C語言只是顯示字符這個行爲的觸發者,只是在代碼中通過系統調用觸發了顯示字符的這個行爲,而顯示字符這個行爲的具體實現,是由操作系統及其後面的一系列設備共同來完成的。
  • 另外,C語言也不具備通過系統調用獲取系統服務的能力,通過系統調用獲取系統服務是由彙編代碼來實現的,嚴格來講,C語言只是作爲彙編代碼的載體,只能勉強算是行爲的觸發者。

第三節 將系統調用封裝成函數

void write(char * A){//參數類型爲指向char類型的指針
    int B = 0;
    for(char * C = A; C ++ != '\0'; B++);
     __asm__(
    	"mov rax, 1 \n\t"
        "mov rdi, 1 \n\t"
        "syscall \n\t"
        :
        :"S"(A),"d"(B)
    );
    
}

int main(){
    write("123\n");
    write("456\n");
    write("789\n");
    
    return 0;
}
  • B記錄字符串中字符的數量(4個),字符的數量並不包括最後的空字符。
  • "S"(A):S表示rsi寄存器,用於存儲要顯示字符(A)的內存地址(首地址)。
  • "d"(B):d表示rdx寄存器,用於存儲要顯示的字符(B)數量。
  • for循環的作用:通過變量B來記錄要顯示的字符的數量。
    - [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bkqwmY3q-1579599880107)(E:\workspace\TyporaProjects\C筆記\C語言\images\第四章 函數庫\3.png)]
    - [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8JVBrcvo-1579599880108)(E:\workspace\TyporaProjects\C筆記\C語言\images\第四章 函數庫\3-2.png)]

第四節 多個源文件組成的程序、翻譯和鏈接

  • fun.c

  • void write(char * A){//參數類型爲指向char類型的指針
        int B = 0;
        for(char * C = A; C ++ != '\0'; B++);
         __asm__(
        	"mov rax, 1 \n\t"
            "mov rdi, 1 \n\t"
            "syscall \n\t"
            :
            :"S"(A),"d"(B)
        );
        
    }
  • main.c

  • void write(* char);
    int main(){
        write("123456789\n");
        return 0;
    }
  • 在main.c程序的開頭添加了程序的原型void write(* char);,故即使沒有函數的主體,在編譯時也不會產生語法錯誤。

  • 怎樣做才能使程序在屏幕上打印出字符串?

    • 1.將fun.c程序複製到main.c程序中。
      - [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mZCDdY1M-1579599880109)(E:\workspace\TyporaProjects\C筆記\C語言\images\第四章 函數庫\4-1.png)]
    • 將main.c程序和fun.c函數同時進行編譯。
      - [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-TBpV8cJP-1579599880110)(E:\workspace\TyporaProjects\C筆記\C語言\images\第四章 函數庫\4-2.png)]
  • 源文件(.c)通過編譯(gcc)生成可執行文件(.out)

    • 編譯的過程分爲兩步:對源文件進行翻譯,翻譯以後得到一個目標文件,再對目標文件進行一個鏈接。
    • 也就是:源文件(.c) ----> 翻譯(gcc -c) ----> 目標文件(.o) ----> 鏈接(gcc) ----> 可執行文件(.out)
      • 翻譯:簡單的來說,就是對源代碼進行語法檢查,將源代碼翻譯爲機器指令(翻譯成中間文件)。
      • 鏈接:將兩個源文件所對應的目標文件進行鏈接,生成一個可執行文件。
  • 在命令行輸入:

    • gcc -c fun.c -masm=intel回車,生成一個fun.o文件
    • gcc -c main.c回車,生成一個main.o文件
    • gcc main.o fun.o回車,生成一個a.out文件
    • gcc fun.o main.c回車,仍然生成一個a.out文件

第五節 函數庫

  • 當一個功能非常重要時,一般會將其封裝爲函數,進行單獨存儲,並生成其對應的目標(中間)文件來方便在其他程序中對其進行調用。
  • 假設目前有fun1.c、fun2.c、fun3.c、main.c四個源程序,其中fun1.c源程序中有彙編代碼,main.c程序分別調用了fun1.c、fun2.c、fun3.c:
    • gcc -c fun1.c fun2.c fun3.c -masm=intel回車,分別生成三個相對應的目標(中間)文件fun1.o、fun2.o、fun3.o
    • gcc -c main.c fun1.o fun2.o fun3.o回車,生成一個可執行文件a.out
  • 如果文件越來越多,對其進行逐個編譯就變得異常麻煩,使用ar r libxxx.a fun1.0 fun2.o fun3.o命令,將fun1.0 fun2.o fun3.o三個文件都存儲到libxxx.a文件中,在編譯時就可以更加方便的進行gcc main.c libxxx.a回車,生成一個可執行文件a.out,其中,以.a結尾的文件稱之爲庫文件,用來存儲函數,故函數庫由此誕生,庫函數則爲函數庫中的函數。
  • 注意:
    • 在給庫文件或者說函數庫起名字的時候,以lib開頭。
    • 使用ar r libxxx.a fun1.0 fun2.o fun3.o命令生成庫文件,當生成的庫文件名已存在時,該命令會將目標文件添加到已存在的庫文件中,如果沒有以命令中lib開頭命名的庫文件,則會自動創建一個庫文件,並將目標文件添加到庫文件中。
    • 若庫文件中已經存在命令中的目標文件,新的目標文件就會將庫文件中已有的目標文件(同名的目標文件)覆蓋掉。

第六節 靜態庫、共享庫

  • 函數庫分爲兩種:靜態庫(.a)和共享庫(.so)。
  • 靜態庫(.a)
    • 如何生成靜態庫?
      • arr r libxxx.a fun1.o fun2.o fun3.o
    • 源文件和靜態庫如何進行鏈接生成可執行程序?
      • gcc main.c libxxx.a
    • 源文件和靜態庫鏈接生成的可執行程序中,包含它所用到的函數的代碼,程序體積相對較大。(注意:可執行程序中只載入了它需要的函數,而不是載入整個靜態庫中的函數)
  • 共享庫(.so)
    • 如何生成共享庫?
      • gcc -shared -o libxxx.so fun1.o fun2.o fun3.o
    • 源文件和共享庫如何進行鏈接生成可執行程序?
      • gcc main.c libxxx.so
    • 源文件和共享鏈接生成的可執行程序中不包含它所用到的函數的代碼,程序體積相對較小。可以簡單的理解爲包含了共享庫的一個引用。
    • 當可執行程序需要調用函數時怎麼辦?
      • 程序啓動以後,當執行到共享庫中的函數時,程序會找到共享庫所在的位置,然後將其所需要的函數從共享庫加載到內存中。
    • 程序到哪裏找到共享庫?
      • export_LD_LIBRARY_PATH = ./
    • 爲什麼叫做共享庫?
      • 假設,有5個程序用到了共享庫中的函數A,並且這5個程序同時在運行,函數A只會被加載1次,這5個程序將共享被加載到內存中的同一個函數A。
      • 優點:體積小,不會造成內存浪費、更新/升級程序更加方便,只需要更新共享庫即可,不需要改動程序本身。

第七節 頭文件、預處理指令、文件包含指令

  • 當我們需要大量的,頻繁的使用函數庫時,在程序的開頭聲明函數原型就變成了一件非常麻煩的事情,那麼該如何彌補這個缺陷,使編程更加輕鬆?
    • 第一步:可以把一個函數庫中所有函數原型的聲明集中到一起,保存爲一個以.h結尾的文件,這個文件稱之爲頭文件。
      • 打開編輯器,將函數的原型寫進去,保存爲.h即可。
    • 第二步:#include "xxx.h"
      • #是預處理指令的標識
      • 預處理指令有很多種,include是其中之一,文件中包含指令
      • 作用:將它自己替換爲它所指定的文件中的內容。
    • 程序的編譯分爲三步:
      • 預處理 ----> 翻譯 -----> 鏈接
        • 預處理:根據指令修改源文件
        • 函數庫
        • 頭文件和文件包含指令相互配合可以使得程序的編寫更加輕鬆。

第八節 API

  • 操作系統給我們提供了很多服務,比如在控制檯顯示字符串的服務,我們把操作系統提供的這些服務稱爲系統服務,有了操作系統提供的服務以後,只需要向操作系統發送服務請求,即可實現很強的功能,再換句話說就是系統服務的存在,就是爲了方便程序的開發。
    - [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vbgVEpvj-1579599880110)(E:\workspace\TyporaProjects\C筆記\C語言\images\第四章 函數庫\8-1.png)]
  • 如何向操作系統請求系統服務?
    • 操作系統提供了獲取系統服務的入口點,程序可以通過入口點向操作系統請求服務(比如linux中的syscall),用來獲取系統服務的入口點,稱之爲“系統調用”。畢竟程序必須要通過它才能調用系統服務。
      • 通過系統調用獲取系統服務的這個動作,由於需要用匯編代碼實現,所以非常的繁瑣,故需要簡化操作,讓程序的開發更方便。
    • 我們可以將發送服務請求的彙編代碼嵌入C程序中,並將其封裝爲函數。
      • 一個系統服務就對應着一個函數,當需要調用系統服務時,就不需要單獨編寫系統調用的代碼,直接調用對應的函數即可。
    • 當程序中使用了大量的函數時,函數原型的聲明也將成爲一個編程負擔。可以將這些函數封裝到庫文件裏面,形成一個函數庫,並創建一個和庫文件相對應的頭文件,用於存儲函數的原型聲明,有了庫文件和頭文件以後,就可以將程序中大量的函數原型的聲明替換爲一條文件包含指令。
    • 操作系統所提供的服務非常強大,程序會大量使用它們,爲了方便,一般只會通過庫函數來使用它們,而這些函數就像是程序通往操作系統的一個入口(接口)。
    • 編寫應用程序就避免不了要通過庫函數調用系統服務,所以我們也把庫函數稱爲應用程序編程接口,即API
    • 並不是所有的庫函數都是用來封裝系統調用的,有些庫函數封裝了系統調用,用於調用系統服務,比如在控制檯顯示字符的函數;有些庫函數則沒有封裝系統調用,比如計算字符串長度的函數,比較兩個數大小的函數,這些函數使用C語言給我們提供的各種運算符和語句就可以實現,所以並不是所有的庫函數都是用來封裝系統調用的。

第九節 POSIX(Unix、Linux)

  • C語言的誕生是爲了重寫UNIX操作系統,UNIX操作系統又是C語言程序設計的重要平臺,所以,爲了在UNIX平臺上使用C語言進行編程更加的方便,快捷,UNIX操作系統中的系統調用,必然會被封裝爲C函數,並進一步的被封裝爲C庫,以方便程序的開發。
  • 隨着UNIX操縱系統的不斷髮展,其版本就越來越多,版本之間的差異也就越來越大,比如,UNIX-A中有一個系統服務A,而UNIX-B中可能就沒有這個服務。它們的系統服務存在差異,則其函數庫也必然存在差異,而各個版本之間的差異,嚴重影響了程序的可移植性。
  • 在這裏插入圖片描述
    • 假設我們在UNIX-A上寫了一個程序,程序中使用了一個庫函數,這個庫函數就調用了系統服務A,那麼這個程序就只能在UNIX-A中運行,而無法在UNIX-B中運行(原因:UNIX-B裏面壓根就沒有這個函數,也沒有這個服務,所以程序也就不具備可移植性)。
  • 後來,IEEE協會制定了一個標準 – POSIX標準(可移植性操作系統接口)。
  • POSIX標準最初的目的是提升應用程序在各種UNIX操作系統之間的可移植性。POSIX定義了操作系統必須提供的系統服務和一個函數庫(並以頭文件的形式發佈),也就是說,它把系統服務和函數庫都統一了起來,簡單來說就是把接口統一了起來。
  • POSIX含義解析(以頭文件的形式發佈是什麼意思)
    • 只提供了頭文件,並沒有提供庫文件,也就是隻定義了函數的原型。只是規定了有多少個函數,函數的名字是什麼,函數的參數是什麼,函數的返回值是什麼,函數實現了什麼功能(函數具體是如何實現的並不進行深究,但是必須要有這個函數,要實現這個功能)。
    • 庫函數是由誰來提供的?
      • 庫函數是由編譯器廠商來提供的,編譯器廠商會根據編譯器所運行平臺的具體情況來實現POSIX所定義的函數,然後,編譯器廠商會把庫文件、頭文件和編譯器打包在一起,故而我們在下載編譯器的時候,裏面就包含了庫文件和頭文件。每一種編譯器都會提供一些流行的函數庫。
    • 正是由於上述原因,我們在UNIX操作系統上進行編程的時候,就可以使用POSIX所定義的函數,這樣,程序就可以在UNIX操作系統之間進行移植。
    • POSIX只是定義了函數原型,並沒有定義函數體,也沒有統一函數的具體實現,這樣會造成什麼影響?
      • 沒有任何影響。
    • LINUX操作系統也支持POSIX標準,所以,編寫運行在UNIX或者LINUX平臺上的程序時,可以使用POSIX所定義的函數。

第十節 POSIX標準庫到底是由誰來提供的

  • POSIX標準是給操作系統制定的,規定了操作系統必須提供的系統調用和函數庫,需要注意的是,函數庫是通過頭文件來規定的。
  • 都有哪些操作系統支持POSIX標準?
    • UNIX和LINUX都支持POSIX標準,所以,UNIX和LINUX都必須按照該標準的要求,提供相應的系統調用及其函數庫,而所謂的提供函數庫,指的就是提供庫文件及其相應的頭文件。
  • POSIX標準是給操作系統制定的,如果某個操作系統支持這個標準,這個操作系統就必須按照標準的要求來提供相應的服務。POSIX標準分別對系統調用和函數庫進行了規定,必須按照要求來提供相應的系統調用和庫文件。所以我們說UNIX和LINUX都按照標準的要求,提供了相應的系統調用及其函數庫。
  • 操作系統提供的函數庫(頭文件、庫文件)和編譯器所提供的函數庫(頭文件、庫文件)之間,有什麼區別
    • 二者提供的函數庫本身並沒有任何區別;
    • 操作系統支持POSIX標準,所以必須提供頭文件和庫文件。而編譯器提供或者不提供都是可以的,沒有一個強制的要求,只不過大多數的編譯器都會提供一些比較流行的函數庫給用戶使用。

第十一節 動態鏈接庫(windows)

  • windows中的函數庫
    - [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dl8cbNRR-1579599880111)(E:\workspace\TyporaProjects\C筆記\C語言\images\第四章 函數庫\11-1.png)]
    • windows是微軟公司的私人產品,和其他的操作系統一樣,爲了能夠更方便的在windows上開發程序,微軟公司把windows的系統調用封裝成了函數,並進一步封裝成了函數庫提供給開發者使用,需要注意的是,windows中的函數庫並不是以.a或者.so結尾的,而是以.dll結尾的,windows將其稱爲動態鏈接庫(Dynamic Linkable Library),動態鏈接庫的性質和LINUX中的共享庫的性質是一致的,都是在程序運行時進行鏈接的,顯而易見,開發基於LINUX操作系統的程序,使用POSIX標準庫將是最好的選擇,使用POSIX標準庫更利於程序在LINUX操作系統之間進行移植;開發基於windows操作系統的程序,使用windows提供的動態鏈接庫將是最好的選擇,由於windows是微軟公司私有的,只有一個系列,並沒有其他的分支,所以在windows中並不存在程序移植的問題。

第十二節 C語言標準的發展脈絡(編譯器)

  • 1973年,C語言誕生,沒有標準

  • 1978年,《C程序設計語言》出版,K&R C / 經典C

  • 1989年,ANSIC(C89),定義了語言本身,定義了函數庫(C標準庫)

  • 1990年,ISO C(C90),ISO C == ANSI C

  • 1999年,ISO C(C99)

  • 2011年,ISO C(C11)

  • UNIX/LINUX提供了POSIX標準庫。

  • windows提供了動態鏈接庫。

  • 其他操作系統提供了其他相應的函數庫來供開發者使用。

  • 如果程序中使用了動態鏈接庫,則只能運行在windows上,如果程序中使用了POSIX標準庫,則只能運行在UNIX/LINUX上,如果程序使用了其他函數庫,則只能運行在函數庫所對應的操作系統上。從當前階段來看,函數庫與操作系統是相關聯的,C語言不具備可移植性。

  • C89、C90、C99、C11等標準是爲C語言制定的,C89、C90、C99、C11等標準定義的函數庫稱爲C標準庫,而POSIX標準是爲操作系統制定的,POSIX標準定義的函數庫稱爲POSIX標準庫。POSIX標準庫是由操作系統提供的,C標準庫與操作系統無關,在實際應用中,C標準庫是由編譯器提供的。
    - [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IIT1a344-1579599880111)(E:\workspace\TyporaProjects\C筆記\C語言\images\第四章 函數庫\12-1.png)]
    總結:

    • 不同點:POSIX標準庫是由操作系統提供的,C標準庫是由編譯器提供的。
    • 相同點:函數庫的定義都是通過頭文件(.h)來實現的。
  • 如何通過C標準庫來實現可移植性?

    • 基本上所有的編譯器都支持C標準庫,運行在windows上的編譯器支持C標準庫,運行在UNIX/LINUX上的編譯器支持C標準庫,運行在其他操作系統上的編譯器也支持C標準庫,所以,如果在程序中使用C標準庫,將程序移植到任何操作系統上,都可以成功運行。
    • 簡單來說,C語言的可移植性是通過編譯器對C標準庫的支持來實現的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章