1. 最簡單的彙編程序

例 18.1. 最簡單的彙編程序

#PURPOSE: Simple program that exits and returns a
#	  status code back to the Linux kernel
#
#INPUT:   none
#
#OUTPUT:  returns a status code. This can be viewed
#	  by typing
#
#	  echo $?
#
#	  after running the program
#
#VARIABLES:
#	  %eax holds the system call number
#	  %ebx holds the return status
#
 .section .data

 .section .text
 .globl _start
_start:
 movl $1, %eax	# this is the linux kernel command
		# number (system call) for exiting
		# a program

 movl $4, %ebx	# this is the status number we will
		# return to the operating system.
		# Change this around and it will
		# return different things to
		# echo $?

 int $0x80	# this wakes up the kernel to run
		# the exit command

把這個程序保存成檔案hello.s(彙編程序通常以.s作為檔案名尾碼),用彙編器(Assembler)as把彙編程序中的助記符翻譯成機器指令,生成目標檔案hello.o

$ as hello.s -o hello.o

然後用連結器(Linker,或Link Editor)ld把目標檔案hello.o連結成執行檔hello

$ ld hello.o -o hello

為什麼用彙編器翻譯成機器指令了還不行,還要有一個連結的步驟呢?連結主要有兩個作用,一是修改目標檔案中的信息,對地址做重定位,在第 5.2 節 “執行檔”詳細解釋,二是把多個目標檔案合併成一個執行檔,在第 2 節 “main函數和啟動常式”詳細解釋。我們這個例子雖然只有一個目標檔案,但也需要經過連結才能成為執行檔。

現在執行這個程序,它只做了一件事就是退出,退出狀態是4,第 2 節 “自定義函數”講過在Shell中可以用特殊變數$?得到上一條命令的退出狀態:

$ ./hello
$ echo $?
4

所以這段彙編代碼相當於在C程序的main函數中return 4;。為什麼會相當呢?我們在第 2 節 “main函數和啟動常式”詳細解釋。

下面逐行分析這個彙編程序。首先,#號表示單行註釋,類似於C語言的//註釋。

 .section .data

彙編程序中以.開頭的名稱並不是指令的助記符,不會被翻譯成機器指令,而是給彙編器一些特殊指示,稱為彙編指示(Assembler Directive)或偽操作(Pseudo-operation),由於它不是真正的指令所以加個“”字。.section指示把代碼劃分成若干個段(Section),程序被操作系統加載執行時,每個段被加載到不同的地址,操作系統對不同的頁面設置不同的讀、寫、執行權限。.data段保存程序的數據,是可讀可寫的,相當於C程序的全局變數。本程序中沒有定義數據,所以.data段是空的。

 .section .text

.text段保存代碼,是隻讀和可執行的,後面那些指令都屬於.text段。

 .globl _start

_start是一個符號(Symbol),符號在彙編程序中代表一個地址,可以用在指令中,彙編程序經過彙編器的處理之後,所有的符號都被替換成它所代表的地址值。在C語言中我們通過變數名訪問一個變數,其實就是讀寫某個地址的內存單元,我們通過函數名調用一個函數,其實就是跳轉到該函數第一條指令所在的地址,所以變數名和函數名都是符號,本質上是代表內存地址的。

.globl指示告訴彙編器,_start這個符號要被連結器用到,所以要在目標檔案的符號表中標記它是一個全局符號(在第 5.1 節 “目標檔案”詳細解釋)。_start就像C程序的main函數一樣特殊,是整個程序的入口,連結器在連結時會查找目標檔案中的_start符號代表的地址,把它設置為整個程序的入口地址,所以每個彙編程序都要提供一個_start符號並且用.globl聲明。如果一個符號沒有用.globl聲明,就表示這個符號不會被連結器用到。

_start:

這裡定義了_start符號,彙編器在翻譯彙編程序時會計算每個數據對象和每條指令的地址,當看到這樣一個符號定義時,就把它後面一條指令的地址作為這個符號所代表的地址。而_start這個符號又比較特殊,它所代表的地址是整個程序的入口地址,所以下一條指令movl $1, %eax就成了程序中第一條被執行的指令。

 movl $1, %eax

這是一條數據傳送指令,這條指令要求CPU內部產生一個數字1並保存到eax寄存器中。mov的尾碼l表示long,說明是32位的傳送指令。這條指令不要求CPU讀內存,1這個數是在CPU內部產生的,稱為立即數(Immediate)。在彙編程序中,立即數前面要加$,寄存器名前面要加%,以便跟符號名區分開。以後我們會看到mov指令還有另外幾種形式,但數據傳送方向都是一樣的,第一個操作數總是源操作數,第二個操作數總是目標操作數。

 movl $4, %ebx

和上一條指令類似,生成一個立即數4並保存到ebx寄存器中。

 int $0x80

前兩條指令都是為這條指令做準備的,執行這條指令時發生以下動作:

  1. int指令稱為軟中斷指令,可以用這條指令故意產生一個異常,上一章講過,異常的處理和中斷類似,CPU從用戶模式切換到特權模式,然後跳轉到內核代碼中執行異常處理程序。

  2. int指令中的立即數0x80是一個參數,在異常處理程序中要根據這個參數決定如何處理,在Linux內核中int $0x80這種異常稱為系統調用(System Call)。內核提供了很多系統服務供用戶程序使用,但這些系統服務不能像庫函數(比如printf)那樣調用,因為在執行用戶程序時CPU處于用戶模式,不能直接調用內核函數,所以需要通過系統調用切換CPU模式,經由異常處理程序進入內核,用戶程序只能通過寄存器傳幾個參數,之後就要按內核設計好的代碼路線走,而不能由用戶程序隨心所欲,想調哪個內核函數就調哪個內核函數,這樣可以保證系統服務被安全地調用。在調用結束之後,CPU再切換回用戶模式,繼續執行int $0x80的下一條指令,在用戶程序看來就像函數調用和返回一樣。

  3. eaxebx的值是傳遞給系統調用的兩個參數。eax的值是系統調用號,Linux的各種系統調用都是由int $0x80指令引發的,內核需要通過eax判斷用戶要調哪個系統調用,_exit的系統調用號是1。ebx的值是傳給_exit的參數,表示退出狀態。大多數系統調用完成之後會返回用戶空間繼續執行後面的指令,而_exit系統調用比較特殊,它會終止掉當前進程,而不是返回用戶空間繼續執行。

x86彙編的兩種語法:intel語法和AT&T語法

x86彙編一直存在兩種不同的語法,在intel的官方文檔中使用intel語法,Windows也使用intel語法,而UNIX平台的彙編器一直使用AT&T語法,所以本書使用AT&T語法。movl %edx,%eax這條指令如果用intel語法來寫,就是mov eax,edx,寄存器名不加%號,源操作數和目標操作數的位置互換,字長也不是用指令的尾碼l表示而是用另外的方式表示。本書不詳細討論這兩種語法之間的區別,讀者可以參考[AssemblyHOWTO]

介紹x86彙編的書很多,UNIX平台的書都採用AT&T語法,例如[GroudUp],其它書一般採用intel語法,例如[x86Assembly]

習題

1、把本節例子中的int $0x80指令去掉,彙編、連結也能通過,但是執行的時候出現段錯誤,你能解釋其原因嗎?