之前我們學習了如何用C標準I/O庫讀寫檔案,本章詳細講解這些I/O操作是怎麼實現的。所有I/O操作最終都是在內核中做的,以前我們用的C標準I/O庫函數最終也是通過系統調用把I/O操作從用戶空間傳給內核,然後讓內核去做I/O操作,本章和下一章會介紹內核中I/O子系統的工作原理。首先看一個打印Hello world的彙編程序,瞭解I/O操作是怎樣通過系統調用傳給內核的。
例 28.1. 彙編程序的Hello world
.data # section declaration msg: .ascii "Hello, world!\n" # our dear string len = . - msg # length of our dear string .text # section declaration # we must export the entry point to the ELF linker or .global _start # loader. They conventionally recognize _start as their # entry point. Use ld -e foo to override the default. _start: # write our string to stdout movl $len,%edx # third argument: message length movl $msg,%ecx # second argument: pointer to message to write movl $1,%ebx # first argument: file handle (stdout) movl $4,%eax # system call number (sys_write) int $0x80 # call kernel # and exit movl $0,%ebx # first argument: exit code movl $1,%eax # system call number (sys_exit) int $0x80 # call kernel
像以前一樣,彙編、連結、運行:
$ as -o hello.o hello.s $ ld -o hello hello.o $ ./hello Hello, world!
這段彙編相當於以下C代碼:
#include <unistd.h> char msg[14] = "Hello, world!\n"; #define len 14 int main(void) { write(1, msg, len); _exit(0); }
.data
段有一個標號msg
,代表字元串"Hello, world!\n"
的首地址,相當於C程序的一個全局變數。注意在C語言中字元串的末尾隱含有一個'\0'
,而彙編指示.ascii
定義的字元串末尾沒有隱含的'\0'
。彙編程序中的len
代表一個常量,它的值由當前地址減去符號msg
所代表的地址得到,換句話說就是字元串"Hello, world!\n"
的長度。現在解釋一下這行代碼中的“.”,彙編器總是從前到後把彙編代碼轉換成目標檔案,在這個過程中維護一個地址計數器,當處理到每個段的開頭時把地址計數器置成0,然後每處理一條彙編指示或指令就把地址計數器增加相應的位元組數,在彙編程序中用“.”可以取出當前地址計數器的值,該值是一個常量。
在_start
中調了兩個系統調用,第一個是write
系統調用,第二個是以前講過的_exit
系統調用。在調write
系統調用時,eax
寄存器保存着write
的系統調用號4,ebx
、ecx
、edx
寄存器分別保存着write
系統調用需要的三個參數。ebx
保存着檔案描述符,進程中每個打開的檔案都用一個編號來標識,稱為檔案描述符,檔案描述符1表示標準輸出,對應于C標準I/O庫的stdout
。ecx
保存着輸出緩衝區的首地址。edx
保存着輸出的位元組數。write
系統調用把從msg
開始的len
個位元組寫到標準輸出。
C代碼中的write
函數是系統調用的包裝函數,其內部實現就是把傳進來的三個參數分別賦給ebx
、ecx
、edx
寄存器,然後執行movl $4,%eax
和int $0x80
兩條指令。這個函數不可能完全用C代碼來寫,因為任何C代碼都不會編譯生成int
指令,所以這個函數有可能是完全用彙編寫的,也可能是用C內聯彙編寫的,甚至可能是一個宏定義(省了參數入棧出棧的步驟)。_exit
函數也是如此,我們講過這些系統調用的包裝函數位于Man Page的第2個Section。