天天看點

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

寫在前面的話

     知識的認知過程其實并不是很輕松的,更何況是Linux系統程式設計這樣一個内容繁雜的話題,足以讓大部分人都感覺到一副霧霭闌珊的畫面。不過不要就此略過下面那些吸引人心的話題。下面的音樂或許能讓這有一個輕松愉快的開始:

    随着悠揚的旋律,我們該思考一下我們用如何認識一下這門知識。但凡走進這篇文章的朋友,多少都會聽說過Linux作業系統,想必也會聯想到黑色的對話視窗,飛快翻滾的白色指令和輸出結果,是的Linux的界面甚是樸素,但其廣泛的應用範圍和其卓越的性能和穩健讓其遍布世界的各個角落。

    然而這個世界就是這樣公平,卓越的表現大都源自于背後的複雜原理和多樣的控制,要想準确無誤的控制Linux,我們用C語言和它進行對話,利用Linux提供的工具附和着其内在的運作原理,我們編織出屬于我們想要的功能。說到這貌似我已經說出了關鍵的三個方面:C語言,API,運作原理。但我想說,這确實是我們大部分程式設計從業人員都很頭痛的地方,的确我們在大學學習過C語言,學習過作業系統,學習過資料結構,但那有什麼用呢?

    想必你聽說過Redis,你也用過Nginx,你更用過MySQL等等各個在業界表現優異的應用程式,但是你想過這些是如何實作的嗎?沒錯就是上面提到的你在大學學過的當時認為不能産生經濟價值的無用知識,這些與目前應用軟體開發有代溝的東西就是我們一路披荊斬棘的聖劍,是你在計算機界乘風破浪的動力源泉,Linux的程式開發便是其一個神奇的應用領域,這裡沒有華麗的程式設計界面,沒有火熱的宣傳市場,沒能讓每一個想邁入計算機行業的年輕人耳熟能詳,但它确是計算機理論連接配接那些奪人眼球的應用市場的關鍵一環,想學好這門知識需要足夠的耐心,需要足夠的堅強,需要足夠的基礎知識,需要我們欠缺的工匠精神,當走過黎明前的黑暗,你就會發現原來MySQL并不那麼遙不可及,Redis也不是不能實作,所有這一切我們都能做得到,而不是僅僅停留在使用層面上,那麼加油吧“少年”!

    我們該如何學習Linux程式設計呢?人類對新事物的認識是有一定的規律的,這個世界上可以說沒有不能了解的知識,隻有描述不夠清晰的教科書。我們的所有定理,自然規律,都是我們前輩的對事物的客觀總結,是人類大腦提煉出來的思維成果,我們隻要具備一定的基礎知識,隻要拿到一個精确詳盡的關于确定問題的描述,我們也是能夠領悟和了解的,是以我将在本文中力求做到對問題的精确描述,将複雜問題和現實社會的容易了解的關系進行類比,将抽象的問題形象化,我們就有的放矢的去了解了。

    學會一項技能正确了解僅是第一步,可以說你隻得到了一半,當你可以運用你了解的知識去指導你進行生産應用,才算是拿到了另一半。是以總的來說學習和認知可簡要的歸為兩部分:了解知識,應用知識。是以關于Linux程式設計的學習離不開實際應用和程式設計實踐,我會在每篇輔以針對本篇内容的應用試驗,提供詳細的代碼和說明,以及運作的條件和結果(反正電子版又不用考慮用紙的成本)向讀者表述出詳細的情形,迅速對知識形成清晰的認識。

    另一個重要的問題我不得不說,Linux的知識很繁雜,我們不能在一開始就要求一步全都學會,對于這類知識我們的經驗是學習主幹知識,為讀者建立清晰的主幹知識網絡,其餘的是在讓讀者學習主幹知識的過程中學會自己汲取Linux程式設計的知識去豐富知識脈絡,因為世界在發展,Linux從沒有停止它的進化,我們最重要的是學會自我認知。

  1. 系統調用

    系統調用是Linux核心為實作作業系統功能而提供的運作于核心态的函數,那麼問題就來了:既然系統調用運作于核心态的(也就是由作業系統自身調用的,使用者程序沒有權限),那我們作為使用者又怎麼能調的動這種需要特權的東西呢?

        在說清這個問題之前我們先說一些概念,來建立我們交流的基礎:

   使用者态和核心态:這兩個是作業系統工作的兩種形式,工作于使用者态的程序,隻能通路自己程序内的空間;工作于核心态的程序權限就比較高,不僅能通路核心态的程序空間,還能通路其他使用者态的程序空間。這就比較有意思了,打個比方,使用者态的程序就相當于每個星巴克的會員顧客,他們隻能看看自己會員卡的積分和消費記錄,但核心态的程序就相當于星巴克的系統管理者,他們能看到每個會員的積分和消費記錄,通過這種權限的區分,能夠實作很多有意思的東西和很多安全保護。

    API和C标準庫: API這個三個字母想必大家可能有所了解,其實這是一種标準化的對話标準(對于Linux來說著名的是POSIX),這是為了便于我們做應用程式開發而制定的一個和Linux系統打交道的标準,我們通過遵從API能夠在不同版本的Linux中不改變應用程式和作業系統的互動方式,這樣就省去了在Linux不斷進化的時候,我們的應用程式不得不改變調用方式的麻煩。那麼C标準庫是實作了主要的API,是以我們的将要學習編寫的C程式都可以使用C标準庫。

    有了上面的描述,我們就可以開始解釋作業系統怎樣允許我們調用隻有核心才能調用的功能(系統調用的工作過程,我們隻需要了解其工作流程即可,詳細的工作過程可參見關于Linux核心的書籍):

    既然我們的程式工作在使用者态,我們還想讓作業系統執行核心實作的功能,這其中必定有一個連接配接的紐帶,它能夠實作函數調用參數的傳遞,核心工作結果的傳回,核心調用哪個函數的選擇功能,這個紐帶我們便成為系統調用,說白了系統調用隻是個中介,真正執行功能的是對應的核心中的服務例程。

    當我們的程式要進行一次系統調用的時候會發生以下的過程:

      1.應用程式通過C庫函數的外殼函數引發系統調用。

    2.外殼函數會把系統調用需要的準備工作做好:首先把需要傳遞給核心服務例程的參數傳遞到CPU寄存器,但是不能多于5個,因為CPU的寄存器數量有限,那要是傳遞的參數多了怎辦,有個辦法就是把使用者程序空間中存儲參數的棧指針放在一個寄存器中傳遞過去,這樣系統調用就能把對應位址的内容根據這個指針讀取過來,注意這時候發生了奇怪的事,那個叫做系統調用的家夥是不是讀取了使用者程序空間的資料?是的,系統調用的工作函數總是system_call(),負責處理使用者程序和核心空間互動資料的一系列工作,它工作在核心态,擁有檢視其它程序内部資料的特權。其次外殼函數還會把此次系統調用到底要調用那個功能的編号(系統調用編号)放到eax寄存器中傳遞給系統調用,至此系統調用的前期準備工作結束。

     3.外殼函數發生軟體中斷 int 0x80,将CPU由使用者态切換到核心态并執行中斷向量處指向的代碼。

   4.很巧,這個中斷向量的代碼便是system_call(),在這之前作業系統自動進行空間轉換為核心态使用者空間的寄存器ss,esp,EFLASG,cs,eip自動壓入核心空間棧。

      5.這時候系統調用例程system_call()就開始一通猛如虎的操作:

            把寄存器中的參數壓入核心棧,因為核心服務例程工作也是以函數的形式進行的,在其被調用之前需要把參數環境準備好,然後把eax中的系統調用編号拿出來和存儲在system_call_table核心變量(需要核心源碼編譯可見):

ENTRY(sys_call_table)

.long SYMBOL_NAME(sys_ni_syscall)

.long SYMBOL_NAME(sys_exit)

.long SYMBOL_NAME(sys_fork)

.long SYMBOL_NAME(sys_read)

.long SYMBOL_NAME(sys_write)

.long SYMBOL_NAME(sys_open)

.long SYMBOL_NAME(sys_close)

.long SYMBOL_NAME(sys_waitpid)

....

其實eax中存儲的就是這裡從0開始計數的行号,通過對應關系找到對應的核心服務例程進行調用,在調用之前會進行對之壓入在核心棧中的參數進行驗證,若合法便會進行對應核心服務例程的調用了,并将傳回結果記錄在errno全局變量中(通常用一個負的傳回值來表明錯誤,傳回一個0值通常表明成功)。

    6.system_call()将傳回值存入eax寄存器。

    7.程序空間轉換為使用者态,自動将寄存器ss,esp,EFLASG,cs,eip自動壓入核心空間棧的數值恢複到對應寄存器。

    8.C庫函數中的外殼函數将對系統調用中設定的errno進行取反,這樣若産生錯誤,errno就成為了正值,無錯誤為0。如果一個系統調用失敗,你可以讀出errno的值來确定問題所在,調用perror()庫函數,可以把該變量翻譯成使用者可以了解的錯誤字元串。

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

  2.庫函數

    庫函數有些完全不使用系統調用,而有些則是對系統調用的包裝,它們往往提供了比系統調用更便利的使用方式,比如printf函數制定了多種樣式的輸出定制,而其包裝的write隻能輸出指定長度的位元組數。

  我們所讨論的是基于标準庫的GNU C(glibc),我們可以用過查找libc.so.6 來執行它以便檢視對應的glibc的版本:

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

上圖顯示目前glibc的版本為2.17

 在運作時狀态我們若想擷取glibc的版本,我們可以使用如下函數:

#include

    const char * gnu_get_libc_version(void) ;  

 它會傳回一個指向字元串的指針,就上圖來講它将傳回“2.17”。

還有一種擷取glibc版本的辦法(glibc特有):

#include

size_t confstr(int name, char *buf, size_t len);

其中name=_CS_GNU_LIBC_VERSION,關于size_t的描述見下文。

下面是實驗,采用上述兩種方法來擷取對應的glibc版本:

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

編譯步驟以及運作結果顯示為:

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

本實驗運作環境為:

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

  3.錯誤處理

    對庫函數和系統調用的傳回值進行測試以監控調用是否正常運作是一個很明智的方法,這能極大地減少程式的調試時間。

    通常對于系統調用傳回值為-1這表明出現錯誤,錯誤的編号為一個正值存儲在了全局變量errno()中(在“系統調用”一節已經描述過),我們可以通過下面兩個函數來顯示具體的發生錯誤的原因:

#include

void perror(const char *s);

該函數直接列印出s參數和errno中數值對應的錯誤描述字元串拼接的字元串結果。

 #include

 char *strerror(int errnum);該函數傳回一個指向對應errnum的錯誤描述字元串,注意該指針指向的内容在多次調用strerror時會對其進行覆寫。

下面是對兩個函數調用的示例:

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

編譯運作結果為:

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

    對于不同的庫函數傳回值也不盡相同:

    與系統調用傳回值相同,-1表示運作異常,errno記錄錯誤編号;

    發生異常時傳回不是-1的值,使用errno記錄錯誤編号;

    根本就不使用errno的;

    上述三種情況都會發生,我們不能詳盡的記住每一個函數的情形,故我們需使用Linux的幫助手冊man(下文會有描述)來檢視使用函數的具體情形。

  4.系統資料類型和可移植性

    來思考一個問題,我們在學習C語言時,經常會看到書上描述int的長度随不同的作業系統實作,能夠容納有符号整型資料類型的範圍也不相同,因為有的系統int為2位元組,有的系統為4位元組,那麼這個問題在Linux家族中也是存在的,這時候就有問題了,我在甲系統上編寫的程式能夠運作,到了另外一個系統上并不一定能夠運作,因為C語言的基礎資料類型實作并不相同,為了解決這個問題SUSV3規定各種标準系統資料類型,并要求Linux的各個實作版本加以定義和使用。

    每種标準系統資料類型的定義均使用了C語言的typedef文法,以程序ID為例其标準系統資料類型為p_id,定義如下:

    typedef int p_id;

      标準系統資料類型都以_t結尾,大多數都定義在sys/types.h中,随着測試宏的不同取值,标準系用資料類型在types.h中的定義會發生變化,故而在編譯程式的時候通過制定測試性宏的指定,便能應對不同系統間C語言基礎資料類型的差異問題:

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

    上面我們介紹了Linux怎樣解決系統資料類型差異的情況下如何定義,那麼還有一個問題發生在列印的時候printf中那麼p_id的比對字元是什麼呢?%d嗎,若是更長我們還要替換為%ld對應為long型,可是系統并不會給我們的苦惱買賬。

    一個相對通用的辦法是利用C語言自動類型轉換的特性對于短于int的類型如short int 在計算和列印時會自動更新為int,對于int和long類型則不會發生詞類轉換,那麼一個笨辦法就是在printf運作時無法判定參數類型時,我們手動的将其轉換為最長的long:

printf("the parent process id is %ld",(long)pid);

便在一定程度解決了這個問題,雖然我們說是笨辦法,卻很簡單實用。

5.使用手冊

    前面我們所描述的錯誤處理,errno的取值,對應描述字元串,函數的格式,需要哪些聲明檔案,都是很具有個性化的,用盡我們半生的力氣也不一定會記清楚,我們不是應試教育,該記的記,不該記的Linux也給我們準備的很清晰,man指令記住了一切:

man 3 perror

便能檢視系統調用的perror函數的所有資訊:

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

怎麼樣,是不是很好用,但是這其中也有一些問題需要我們去解決,那個3是什麼:

    1-Standard commands (标準指令)

    2-System calls (系統調用)

    3-Library functions (庫函數)

    4-Special devices (裝置說明)

    5-File formats (檔案格式)

    6-Games and toys (遊戲和娛樂)

    7-Miscellaneous (雜項)

    8-Administrative Commands (管理者指令)

    9-其他(Linux特定的), 用來存放核心例行程式的文檔

 有了這個解釋清單我們便清楚了,要看庫函數便将man的參數取值為3,若看系統調用便将man的參數取值為2。

    但有的時候,使用 man 2 read 系統并不買賬,我們可能會看不到read系統調用的詳細資訊,這是因為運作的系統沒有安裝man-page:

yum install -y man-page

mandb

安裝man-page後,運作mandb,更新資料庫後,便能看到全部的手冊幫助資訊了。

文章若疏漏錯誤,請各位看官批評指正,我們的公衆号會持續更新Linux C程式設計系列文章,若對此有興趣可關注公衆号:

call stack是什麼錯誤_Linux C程式設計(一)系統調用、系統類型、錯誤處理

繼續閱讀