1. 程式人生 > >裝載、連結與庫——系統呼叫

裝載、連結與庫——系統呼叫

*昨天總結了main函式前世今生的問題,跟著原始碼一步步看。。。今天來看看系統呼叫是什麼。。。→_→*

系統呼叫(System Call)是應用程式與作業系統與核心之間的介面

  1. 系統呼叫(System Call)的定義

    • 現代作業系統將可能產生衝突的系統資源(包括檔案、I/O等裝置)保護起來,阻止應用程式直接訪問

    • 為了讓應用程式有能力訪問系統資源,也為了讓程式藉助作業系統做一些必須由作業系統支援的行為,每個作業系統都提供一套介面,以供應用程式使用

    • 這些介面往往通過中斷實現,比如Linux使用0x80號埠作為系統呼叫的入口,Windows採用0x2E號中斷作為系統呼叫入口

  2. 系統呼叫的弊端

    • 使用不便

      :作業系統提供的系統呼叫介面過於原始,沒有進行很好的包裝,使用起來不方便

    • 各個作業系統之間呼叫不相容

    • 解決方法:執行庫作為系統呼叫與程式之間的抽象層,可以簡化使用,統一形式

    • 執行時庫將不同的作業系統的系統呼叫包裝成統一固定的介面,使得同樣的程式碼在不同的作業系統下都可以直接編譯併產生一致的效果,即原始碼級別上的可移植性

  3. 系統呼叫的原理

    • 現代作業系統中通常有兩種特權級別:使用者態(User Mode)和核心態(Kernel Mode)。作業系統根據不同的特權,使不同的程式碼執行在不同的模式上以限制其權利,提高穩定性和安全性

    • 系統呼叫是執行在核心態的,而應用程式基本都是執行在使用者態的

    • 作業系統一般通過中斷(Interrupt)來從使用者態切換到核心態

    • 中斷是一個硬體或軟體發出的請求,要求CPU暫停當前的工作轉手去處理更重要的事情。中斷一般具有兩個屬性,一個稱為中斷號(從0開始),一個稱為中斷處理程式(Interrupt Service Routine, ISR)不同的中斷具有不同的中斷號,而中斷處理程式又與中斷號一一對應。在核心中,有一個數組稱為中斷向量表(Interrupt Vector Table)這個陣列的第n項包含了指向第n號中斷的中斷處理程式指標

    • 當中斷到來時,CPU會暫停執行當前執行的程式碼,根據中斷的中斷號,在中斷向量表中找到對應的中斷處理程式,並呼叫它。中斷處理程式執行完成之後,CPU會繼續執行之前的程式碼

      CPU中斷過程

  4. Linux系統呼叫流程

    • 在x86下,系統呼叫由0x80中斷完成,各個通用暫存器用於傳遞引數,EAX暫存器用於表示系統呼叫的介面號。當系統呼叫返回時,EAX暫存器又作為呼叫結果的返回值

    • 每個系統呼叫都對應於核心原始碼中的一個函式,他們都以“sys_”開頭。(定義路徑:linux-2.4.0\include\asm-i386\Unistd.h)

    • 基於int的Linux經典系統呼叫實現

      Linux系統中斷流程

  5. Linux系統呼叫原始碼剖析

    • _syscall0巨集函式:無參的系統呼叫的封裝第一個引數為系統呼叫的返回值型別,第二個引數是系統呼叫的的名稱,其展開後形成一個與系統呼叫名稱同名的函式

      //linux-2.4.0\include\asm-i386\Unistd.h 
      
      #define _syscall0(type,name) \
      
      type name(void) \
      { \
      long __res; \
      __asm__ volatile ("int $0x80" \//volatile防止編譯器對程式碼優化
              : "=a" (__res) \//表示用EAX輸出返回資料並存儲在__res中
              : "0" (__NR_##name)); \//表示和輸出相同的暫存器EAX傳遞引數
      __syscall_return(type,__res); \
      }
    • _syscall1巨集函式帶有一個引數的系統呼叫的封裝,通過EBX暫存器傳入。x86下的Linux支援的系統呼叫引數至多有6個,分別使用6個暫存器來傳遞引數(EBX、ECX、EDX、ESI、EDI、EBP)

      //linux-2.4.0\include\asm-i386\Unistd.h 
      
      #define _syscall1(type,name,type1,arg1) \
      
      type name(type1 arg1) \
      { \
      long __res; \
      __asm__ volatile ("int $0x80" \
          : "=a" (__res) \
          //把arg1強制轉化為long,然後存放在EBX裡作為輸入。編譯器也會生成相應的程式碼保護原來的EBX的值不被破壞
          : "0" (__NR_##name),"b" ((long)(arg1))); \
      __syscall_return(type,__res); \
      }
    • __syscall_return巨集函式:用於檢查系統呼叫返回值,並將其轉化為C語言的errno錯誤碼。在Linux中系統呼叫使用返回值傳遞錯誤碼,如果返回值為負數,表明呼叫失敗,返回值的絕對值就是錯誤碼C語言中大多數函式以返回-1表示呼叫失敗,將錯誤資訊儲存在名為errno的全域性變數中

      
      #define __syscall_return(type, res)                 \
      
      do {                                    \
          if ((unsigned long)(res) >= (unsigned long)(-125)) {        \
              errno = -(res);                     \
              res = -1;                       \
          }                               \
          return (type) (res);                        \
      } while (0)
    • 切換堆疊:在實際執行中斷向量表中的第0x80號元素所對應的函式之前,CPU還要進行棧切換。在Linux中,使用者態和核心態使用不同的棧,兩者各自負責各自的函式呼叫。所謂當前棧即ESP所指的棧空間暫存器SS儲存當前棧所在的頁

      使用者棧切換至核心棧步驟如下:

      • 儲存當前ESP、SS的值
      • 將ESP、SS的值設定為核心棧的相應值

      核心棧切換至使用者棧步驟如下:

      • 恢復原來的ESP、SS的值
      • 使用者態的ESP和SS的值儲存在核心棧上,由中斷指令自動地由硬體完成

      CPU除了切入核心態之外,還自動完成:

      • 找到當前程序地核心棧(每一個程序都有自己的核心棧)
      • 在核心棧中依次壓入使用者態的寄存去SS、ESP、EFLAGS、CS、EIP

      當核心從系統呼叫中返回時,需呼叫iret指令返回到使用者態,iret指令會從核心棧中彈出暫存器SS、ESP、EFLAGS、CS、EIP的值,使棧恢復到使用者態的狀態

      中斷時使用者棧和核心棧切換

    • 中斷處理程式在int指令切換棧之後,程式流程就切換到了中斷向量表中記錄的0x80號中斷處理程式

      Linux i386 中斷服務流程

      初始化中斷向量表

      //linux-2.4.0\arch\i386\kernel\Traps.c
      //0-19號中斷對應的中斷處理程式包括算數異常(除零、溢位)、頁缺失(page fault)、無效指令
      
      void __init trap_init(void)
      {
          ......
      
          set_trap_gate(0,&divide_error);
          set_trap_gate(1,&debug);
          set_intr_gate(2,&nmi);
          set_system_gate(3,&int3);   /* int3-5 can be called from all */
          set_system_gate(4,&overflow);
          set_system_gate(5,&bounds);
          set_trap_gate(6,&invalid_op);
          set_trap_gate(7,&device_not_available);
          set_trap_gate(8,&double_fault);
          set_trap_gate(9,&coprocessor_segment_overrun);
          set_trap_gate(10,&invalid_TSS);
          set_trap_gate(11,&segment_not_present);
          set_trap_gate(12,&stack_segment);
          set_trap_gate(13,&general_protection);
          set_trap_gate(14,&page_fault);
          set_trap_gate(15,&spurious_interrupt_bug);
          set_trap_gate(16,&coprocessor_error);
          set_trap_gate(17,&alignment_check);
          set_trap_gate(18,&machine_check);
          set_trap_gate(19,&simd_coprocessor_error);
      
          //系統呼叫對應的中斷號,在linux-2.4.0\include\asm-i386\Hw_irq.h中,SYSCALL_VECTOR定義0x80
          set_system_gate(SYSCALL_VECTOR,&system_call);
      
          ......
      }

      呼叫int 0x80之後,最終執行system_call函式

      ENTRY(system_call)
          ......
          SAVE_ALL//巨集函式SAVE_ALL將各種暫存器壓入棧中,即系統呼叫傳入的引數
          ......
          cmpl $(nr_syscalls), %eax//比較EAX和nr_syscalls,nr_syscalls是比最大的呼叫號大1的值,如果eax(使用者傳入的系統呼叫號)大於等於nr_syscalls,則這個系統呼叫無效,則會跳轉至syscall_badsys執行,反之執行syscall_call
          jae syscall_badsys
      
      syscall_call:
          call *sys_call_table(0,%eax,4)//系統呼叫表中,每一個元素(long型別)都是一個系統呼叫函式的地址。因此呼叫的是sys_call_table上偏移量為0+%eax*4上的元素的值指向的函式
      
          ......
          RESTORE_REGS//執行結束後,使用巨集函式恢復之前SAVE_ALL儲存的的暫存器
          ......
          iret//最後通過指令iter從中斷處理程式中返回

      Linux系統呼叫流程

綜上所述,Linux系統呼叫流程為:main -> function -> _syscall -> int 0x80 -> __init trap_init -> system_call -> __syscall_return

*so happy…so tired…→_→*