1. 彙編程序的Hello world

之前我們學習了如何用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,ebxecxedx寄存器分別保存着write系統調用需要的三個參數。ebx保存着檔案描述符,進程中每個打開的檔案都用一個編號來標識,稱為檔案描述符,檔案描述符1表示標準輸出,對應于C標準I/O庫的stdoutecx保存着輸出緩衝區的首地址。edx保存着輸出的位元組數。write系統調用把從msg開始的len個位元組寫到標準輸出。

C代碼中的write函數是系統調用的包裝函數,其內部實現就是把傳進來的三個參數分別賦給ebxecxedx寄存器,然後執行movl $4,%eaxint $0x80兩條指令。這個函數不可能完全用C代碼來寫,因為任何C代碼都不會編譯生成int指令,所以這個函數有可能是完全用彙編寫的,也可能是用C內聯彙編寫的,甚至可能是一個宏定義(省了參數入棧出棧的步驟)。_exit函數也是如此,我們講過這些系統調用的包裝函數位于Man Page的第2個Section。