UNIX/LINUX,Linux操作系統原理與應用06:系統調用

 2023-10-15 阅读 35 评论 0

摘要:目錄 1. Linux中的各種接口 1.1 LSB標準 1.2 Linux API 1.2.1 概述 1.2.2 Linux內核系統調用接口 1.2.3 C標準庫 1.3 Linux ABI 1.4 內核API 1.5 系統調用與各種接口的關系 1.5.1 系統調用與API的關系 1.5.2 系統調用與系統命令的關系 1.5.3 系統調用與內核函數的關系 2. 中斷

目錄

1. Linux中的各種接口

1.1 LSB標準

1.2 Linux API

1.2.1 概述

1.2.2 Linux內核系統調用接口

1.2.3 C標準庫

1.3 Linux ABI

1.4 內核API

1.5 系統調用與各種接口的關系

1.5.1 系統調用與API的關系

1.5.2 系統調用與系統命令的關系

1.5.3 系統調用與內核函數的關系

2. 中斷、異常和系統調用的比較

2.1 源頭不同

2.2 服務響應方式不同

2.3 處理機制不同

3. 系統調用的基本概念

3.1 系統調用號

3.2 系統調用表

3.3 系統調用封裝例程和服務例程

4. 系統調用處理流程

4.1 設置異常處理函數

4.2 觸發軟中斷進入內核態

4.3 調用系統調用服務例程

4.4 系統調用返回

5. 添加新的系統調用

5.1 添加系統調用號

5.1 添加系統調用表項

5.3 實現系統調用服務例程

5.4 重新編譯內核

5.5 編寫用戶態程序


1. Linux中的各種接口

1.1 LSB標準

① LSB即Linux Standards Base,是Linux標準化領域中事實上的標準

② 由于Linux的發行版眾多,為了促進Linux不同發行版之間的兼容性,LSB開發了一系列標準,使各種軟件可以在兼容LSB標準的系統上運行

1.2 Linux API

1.2.1 概述

Linux API是Linux內核與用戶空間的API,也就是讓用戶空間的程序能夠通過這個接口訪問系統資源和內核提供的服務

Linux API由兩部分組成:Linux內核系統調用接口和GNU C庫(glibc庫)中的例程

1.2.2 Linux內核系統調用接口

系統調用接口是內核中所有已實現的可用系統調用的集合

1.2.3 C標準庫

GNU C庫是Linux內核系統調用接口的封裝,其中包括POSIX兼容的應用函數調用和Linux專用的應用函數調用

目前最新的Linux內核5.0版本中系統調用大約有380個,GNU C庫大約有2000個函數

說明:POSIX標準

POSIX表示可移植操作系統接口(Portable Operating System Interface of UNIX),POISX標準是針對API而不是針對系統調用的,他定義了操作系統應該為應用程序提供的接口標準,并不涉及對應的函數如何實現

1.3 Linux ABI

ABI是一些列約定的集合,可以說調用慣例(calling convention)就是ABI。ABI是和具體的CPU架構和OS相關的,具體而言,ABI包括以下內容,

① 一個特定的處理器指令集

② 函數調用慣例

③ 系統調用方式

④ 可執行文件的格式(e.g. ELF、PE)

說明:什么是函數調用慣例和系統調用方式

Linux提供了一個syscall函數,用來根據系統調用號直接調用系統調用,在該函數的man手冊中說明了不同體系結構觸發軟中斷的命令以及系統調用的傳參方式,其實這就是所謂的ABI

1.4 內核API

① 內核API主要是內核中標記為EXPORT_SYMBOL的函數,這些函數主要是為了內核模塊的編寫而提供的

② 收到內核版本更迭的影響,內核API并不穩定,3.x版本內核的模塊可能在4.x版本上就無法使用

1.5 系統調用與各種接口的關系

1.5.1 系統調用與API的關系

由于API是對系統調用的封裝,所以API和系統調用之間可能存在如下幾種關系,

① API和系統調用形式一致

e.g. read函數和read系統調用

② 幾個不同的API內部使用了同一個系統調用

e.g. malloc / calloc和free函數的實現都調用了brk系統調用

③ 一個API內部使用了多個系統調用

e.g. malloc函數的實現根據要分配內存的大小,可能使用brk系統調用,也可能使用mmap系統調用

④ API內部不使用系統調用

e.g. string.h中聲明的各種字符串處理函數

1.5.2 系統調用與系統命令的關系

系統命令相對API接口更高一層,每個系統命令都是一個可執行程序,使用strace命令可以查看系統命令使用的系統調用

說明:下圖列出了一些系統命令與Linux各模塊之間的關系

1.5.3 系統調用與內核函數的關系

系統調用進入內核后,會找到各自對應的內核函數,這些內核函數被稱為系統調用的服務例程

e.g. 系統調用getpid對應的服務例程為sys_getpid

2. 中斷、異常和系統調用的比較

根據中斷和異常章節的學習可知,中斷、異常和系統調用本質上屬于一類,在處理方式上也類似,他們的差異體現在如下方面

2.1 源頭不同

① 中斷是外設發出的請求

② 異常是應用程序意想不到的行為

③ 系統調用是應用程序請求操作系統提供服務

2.2 服務響應方式不同

① 中斷是異步的

② 異常是同步的

③ 系統調用既可以是異步的(e.g. 異步IO),也可以是同步的

2.3 處理機制不同

① 中斷服務程序在內核態運行,對用戶是透明的

② 異常出現時,或者殺死進程,或者重新執行引起異常的指令

③ 系統調用是用戶發出請求后等待操作系統的服務

3. 系統調用的基本概念

3.1 系統調用號

① 定義在各體系結構的unistd.h中,在Linux 2.6.11 + 80386版本中為include/asm-i386/unistd.h

② 用來唯一的表示每個系統調用

③ 作為系統調用表的下標,當用戶空間的進程執行一個系統調用時,該系統調用號作為參數傳遞,用來指明要執行的系統調用服務例程

3.2 系統調用表

① 在Linux 2.6.11 + 80386版本中,系統調用表sys_call_table定義在arch/i386/kernel/entry.S中

② 系統調用表是一個函數指針數組,使用系統調用號索引

說明:系統調用號和系統調用表一旦分配好就不能有改變,否則編譯好的應用程序會因為調用到錯誤的系統調用而導致程序異常

3.3 系統調用封裝例程和服務例程

① 系統調用服務例程就是內核態最終處理系統調用請求的函數

② 引入系統調用封裝例程是因為用戶空間的程序無法直接調用內核代碼,因此在需要執行一個系統調用時,是通過軟中斷引發一個異常進入內核態,封裝例程就是對這個過程的封裝

4. 系統調用處理流程

UNIX/LINUX,

4.1 設置異常處理函數

在內核初始化的trap_init函數中,將系統調用異常處理函數設置為system_call

說明:將系統調用的IDT描述符設置為系統門,使得用戶態可以穿過該門進入內核態

4.2 觸發軟中斷進入內核態

根據不同的體系結構,使用int $0x80 / syscall / svc指令即可觸發系統調用對應的異常,并跳轉到system_call函數運行

system_call函數在調用系統調用服務例程之前完成如下工作,

① 將系統調用號壓棧(根據ABI,系統調用號通過eax寄存器傳遞)

② 調用SAVE_ALL將異常處理程序可以用到的所有寄存器保存到相應的棧中

此處注意3點,

a. 由于系統調用一定是從用戶態切換到內核態,所以在進入異常處理時,硬件會進行棧的切換,并自動保存相關寄存器,如下圖所示(系統調用中沒有ERROR CODE)

b. SAVE_ALL中同時將ds和es裝入內核數據段的段選擇符

c. 根據之前的系統調用ABI說明,SAVE_ALL保存的寄存器中就包含了系統調用參數

③ 調用GET_THREAD_INFO,將當前進程thread_info的地址存放在ebp中

④ 判斷系統調用號的合法性,如果合法則查找系統調用表并調用系統調用服務例程

4.3 調用系統調用服務例程

① 系統調用服務例程根據系統調用號在sys_call_table中查表得到

② 所有系統調用服務例程的參數為struct pt_regs類型,該類型對應的就是寄存器在棧上的布局

這里就有一個問題了,我們知道在X86體系結構中,函數參數是優先通過寄存器傳遞的,那么給系統調用服務例程的參數是如何傳遞的呢 ? 這里的玄機就在于asmlinkage宏

__attribute__((regparm(n)))用于指定最多可以使用n個寄存器傳遞參數,超過n的參數將使用棧傳遞

對于系統調用,使用regparm(0),也就是所有參數均通過棧傳遞,內核中所有系統調用的實現都使用了這個修飾符

說明:由于ARM體系結構中定義了ATPCS標準,函數的前4個參數使用r0 ~ r4寄存器傳遞,所以asmlinkage宏實際上就是extern "C",并未使用regparm修飾

在調用實際的系統調用服務例程之前,會先將sp指針傳遞給r0

4.4 系統調用返回

當服務例程執行結束時,system_call從eax獲得他的返回值,并把這個返回值存放在棧中,讓其位于用戶態eax寄存器曾存放的位置,然后執行syscall_exit終止系統調用處理程序

當進程恢復到用戶態執行時,就可以從eax中找到系統調用的返回值

說明:系統調用機制優化簡介

在2.6的早期版本中,系統調用的實現使用的是int 0x80和iret命令,因為需要從用戶態切換到內核態執行服務例程,然后再返回用戶態,所以開銷很大

為了加快系統調用的速度,隨后引入了vsyscalls和vDSO機制,這兩種機制都是從機制上對系統調用的速度進行了優化,但是使用軟中斷來進行系統調用需要進行特權級切換這一根本問題并沒有解決

目前X86_64體系結構使用syscall / sysret指令實現系統調用,細節就不介紹了,因為我目前也不懂

說明2:定義系統調用服務例程

在后續的Linux內核版本中,提供了一組宏,用于定義系統調用服務例程

其中宏名中的數字表示服務例程的參數個數,下面舉例說明

該宏可定義sys_write函數

5. 添加新的系統調用

說明:如上文所述,根據不同的體系結構與內核版本,要修改的文件位置會有所不同,甚至要修改的文件就不同,但是思路是一致的

5.1 添加系統調用號

修改體系結構目錄中的unistd.h文件,增加系統調用號

注意同步修改NR_syscalls宏,該宏表示系統調用個數,會用于判斷系統調用號的合法性

5.1 添加系統調用表項

修改體系結構目錄的entry.S文件,添加系統調用表項

5.3 實現系統調用服務例程

可以新建文件,也可以在原有文件中添加系統調用服務例程。如果新建文件,注意修改Makefile

此處我們選擇在kernel/sys.c中新增服務例程,

注意使用asmlinage修飾符

5.4 重新編譯內核

由于修改了內核源碼,要使其生效,必須重新編譯并布署內核

5.5 編寫用戶態程序

#include <unistd.h>
#include <sys/syscall.h>#define __NR_mysyscall 289int main(void)
{syscall(__NR_mysyscall);return 0;
}

此處使用syscall + 系統調用號的方式調用系統調用,并未提供系統封裝例程。在Linux 2.6.18版本之前,unistd.h中提供了一組__syscall宏用于定義系統調用封裝例程,下面以定義3個參數的封裝例程為例加以說明

其中type & name為封裝例程的返回值與函數名,之后的type & arg對用于定義函數形參

在封裝例程的實現中,就是按照ABI的約定將參數通過寄存器傳輸,并調用int $0x80觸發軟中斷

這里特別說明下紅框中的"0",他表示輸入部分仍使用和輸出部分相同的寄存器,次數就是用eax傳輸系統調用號(系統調用號由__NR_##name構成)

說明:增加有套路,定義需謹慎

增加一個系統調用并不難,他有一套比較規范的方法,難點是在實際應用中如何增加合適的系統調用。在絕大多數情況下,我們不會新增系統調用

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/136549.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息