例 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
前兩條指令都是為這條指令做準備的,執行這條指令時發生以下動作:
int
指令稱為軟中斷指令,可以用這條指令故意產生一個異常,上一章講過,異常的處理和中斷類似,CPU從用戶模式切換到特權模式,然後跳轉到內核代碼中執行異常處理程序。
int
指令中的立即數0x80是一個參數,在異常處理程序中要根據這個參數決定如何處理,在Linux內核中int $0x80
這種異常稱為系統調用(System Call)。內核提供了很多系統服務供用戶程序使用,但這些系統服務不能像庫函數(比如printf
)那樣調用,因為在執行用戶程序時CPU處于用戶模式,不能直接調用內核函數,所以需要通過系統調用切換CPU模式,經由異常處理程序進入內核,用戶程序只能通過寄存器傳幾個參數,之後就要按內核設計好的代碼路線走,而不能由用戶程序隨心所欲,想調哪個內核函數就調哪個內核函數,這樣可以保證系統服務被安全地調用。在調用結束之後,CPU再切換回用戶模式,繼續執行int $0x80
的下一條指令,在用戶程序看來就像函數調用和返回一樣。
eax
和ebx
的值是傳遞給系統調用的兩個參數。eax
的值是系統調用號,Linux的各種系統調用都是由int $0x80
指令引發的,內核需要通過eax
判斷用戶要調哪個系統調用,_exit
的系統調用號是1。ebx
的值是傳給_exit
的參數,表示退出狀態。大多數系統調用完成之後會返回用戶空間繼續執行後面的指令,而_exit
系統調用比較特殊,它會終止掉當前進程,而不是返回用戶空間繼續執行。
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]。