主要描述三方面的內容:第一是彙編語言的程序模版,以及模版涉及到的一些知識點;第二是如何調試彙編語言;第三是如何在彙編語言中調用C庫函數。html
彙編語言由段(section)組成,一個程序中執行的代碼,叫文本段(text),程序還可能有定義變量,有付給初始值的變量放在數據段(data)中,沒有賦初值或者付給零初值的放在bss段中。text段必定是要有的,data和bss能夠沒有。linux
用.section語法定義段。好比:編程
.section .text定義文本段,app
.section .data定義數據段,函數
.section .bss定義bss段。工具
順序沒有必須的要求,但爲了便於別人接手和理解你的程序,書中建議採用從上到下按照data,bss,text段的順序定義。測試
文本段必需要定義一個程序執行的起始點,ld默認爲_start;而gcc默認會鏈接標準的庫代碼,該代碼入口爲_start,執行一段程序後跳到main執行,所以gcc默認要求外部源程序定義main,且不能定義_start,但若是使用參數-nostdlib,那麼就不會默認鏈接標準的庫代碼,此時入口點用_start也沒問題;此外,ld和gcc都支持-e參數來指定入口點,此時任意的標號均可以用做入口點。ui
下面以例子來講明。以彙編語言程序設計讀書筆記(2)- 相關工具64位系統篇中的cpuid2.s爲例子進行說明,目前可能讀者還不理解該程序的細節,但本文後面會論述,讀完本文後,理解該程序不是問題。目前只要清楚該程序用於輸出CPU的ID廠商的字符串。源程序入口爲_start。以下:this
cpuid2.s
下面分別對入口爲_start,main,xxxx(任意標籤)這三種狀況下,用as,ld和gcc彙編cpuid2.s,生成可執行文件。編碼
用as,ld生成可執行文件(入口爲_start),以下圖所示:
gcc生成可執行文件(入口爲_start),以下圖:
源程序把標籤_start改成main。此時ld可使用-e main參數,那麼就會以main爲程序起始點;gcc則有兩種方式,一是使用默認的編譯方式,即不帶-nostdlib參數,此時gcc會鏈接庫代碼,因此生成的執行文件的大小較大,另外一種方式是使用-nostdlib,可是用-e main指定入口點。
用as,ld生成可執行文件(入口爲main),以下圖所示:
gcc按第一種方式生成可執行文件(入口爲main),以下圖:
gcc按第二種方式生成可執行文件(入口爲main),以下圖:
第一種方法生成的大小爲4799,第二生成的大小爲2133。
cpuid2.s源程序把_start改成xxxx,即入口爲xxxx。此時必須使用-e xxxx來彙編或者編譯。
用as,ld生成可執行文件(入口爲xxxx),以下圖所示:
gcc生成可執行文件(入口爲xxxx),以下圖:
可見入口爲xxxx的方式包含了以上入口爲_start和main的狀況。
綜合以上狀況,不管入口點是什麼標籤,不管是運行在32位系統或者64位系統,均可以按照如下的命令來彙編和編譯彙編程序。
# 假設要彙編或編譯的彙編程序有n個,爲input_file1.s,input_file2.s,...,_filen.s。n大於等於1。
# 輸入文件列表input_file1.s,...input_filen.s使用{input_file.s}表示,一樣,{input_file.o}表示一系列的.o文件
# 輸出可執行文件爲output_file。
# libc.so所在的絕對路徑用/libc_path表示,ld-linux.so.2所在的絕對路徑用/ld-linux_path表示。
# 入口點標籤爲entry_point。
# []括起來的是調用了C庫函數才須要的部分,去過沒有調用C庫,則不須要。
# 那麼as,ld生成可執行文件的命令以下:
as --32 -o input_file1.o input_file1.s
as --32 -o input_file2.o input_file2.s
...
as --32 -o input_filen.o input_filen.s
ld -m elf_i386 -e entry_point [-dynamic-linker /ld-linux_path/ld-linux.so.2] -o output_file [-L/libc_path -lc] {input_file.o}
# gcc生成可執行文件的命令以下:
gcc -m32 -e entry_point -nostdlib -o output_file [-L/libc_path -lc] {input_file.s}
#注:若是源程序沒有調用C庫函數而又使用了[]中的指令鏈接或編譯,那麼會產生錯誤「/usr/lib/libc.so.1: bad ELF interpreter: 沒有那個文件或目錄」
若是一個彙編程序文件中的代碼調用了另外一個彙編程序文件的標號或者函數,那麼必須聲明這個標號或函數爲.globl(應該是global的縮寫,全局的,能夠跨文件調用)的。
好比彙編程序test3.s和test4.s,test3.s調用了test4.s的函數fun4,若是test4.s沒有.globl fun4這行,那麼編譯會提示錯誤,加上這行則沒有任何問題。
test3.s
test4.s
#符號把.globl fun4註釋了,那麼彙編成test3.o和test4.o後鏈接,會提示在函數_start中,沒有定義fun4。以下圖:
把#去掉後,讓.globl fun4起做用,則沒有任何問題,以下圖:
綜上,任何的標號或者函數,若是要準備給別的文件調用,那麼必定要用.globl聲明。
通過上面的論述後,很容易獲得了彙編程序的模版,以下:
.section .data
<初始化值的數據在這裏>
.section .bss
<未初始化的數據在這裏>
.section .text
.globl entry_point
entry_point:
<代碼指令在這裏>
其中,entry_point爲程序起始點。
書中的範例是用CPUID彙編指令去讀取CPU的廠商ID(Vendor ID)。瞭解這個程序以前先簡單說明幾個知識點。
輸入參數經過寄存器EAX傳入,執行CPUID後,輸出經過EBX,ECX,EDX傳出。這裏只要瞭解EAX=0時,ECX,EDX,EBX分別獲得廠商ID的字符串的高4字節,中間4字節,低4字節。廠商ID的字符串按照小端排列,即先放低字節,即廠商ID爲[EBX][EDX][ECX]。
這能夠經過對CPUID的測試代碼test_cpuid.s來進一步瞭解,以下代碼:
test_cpuid.s
帶調試參數-gstabs生成執行文件後用kdbg調試,在CPUID指令後設置斷點,以下圖:
寄存器ebx,edx,ecx,每一個字節都是ASCII的編碼,把這些字節編碼按照ebx,edx,ecx從低字節到高字節的排列翻譯成字符串,以下圖,顯示以下:
即廠商ID是「GenuineIntel」。
經過0x80號的軟件中斷(int $0x80),能夠調用Linux的內核函數,具體是哪一個內核函數,由EAX寄存器決定,而傳遞給函數的參數則根據調用的函數而有不一樣的含義,通常由EBX,ECX,EDX來傳遞。書籍要到第十二章纔會進一步討論。目前使用到的兩個系統調用先要簡單瞭解。
第一個是第1號調用,調用的是退出函數sys_exit(ret),EAX=1表示調用號,EBX=ret傳遞第一個參數,表示返回給父進程的返回值,即sys_exit(ret)至關於以下的彙編代碼:
# sys_exit(ret)系統調用的彙編代碼
movl $1, %eax
movl $ret, %ebx
int $0x80
第二個調用是第4號調用,調用的是函數sys_write(int fd, const void *buf, size_t count),三個參數分別用ebx,ecx,edx傳入,分別表明文件描述符,要寫的緩衝區首地址,緩衝區字節長度。衆所周知,Linux中用文件描述符1用於表示標準輸出(stdout),默認即顯示終端,所以要往顯示終端打印長度爲length的字符串str,即sys_write(1, str, length)至關於如下的彙編代碼:
# sys_write(1, str, length)系統調用的彙編代碼
movl $4, %eax
movl $1, %ebx
movl $str, %ecx
movl $length, %edx
int $0x80
以下的代碼cpuid.s,讀取CPU的廠商ID,而後打印到屏幕上。代碼爲:
cpuid.s
生成可執行文件以及執行的結果以下圖:
理解了上述的1)和2)所述的知識,加上代碼中的註釋,這段代碼不難理解,前提是須要有一點點彙編語言的基礎(不會movl都不知道吧?),再也不贅述。
用kdbg調試程序能夠不用記gdb的指令,kdbg整個操做界面十分明朗,要單步運行,設置斷點,觀察變量,寄存器,內存,都沒有問題,以下圖:
但有一個特別須要注意的地方,_start以後的第一個nop指令,若是沒有,無法觀察內存,會提示超出範圍,後來把nop加上則一切正常,該書上提到沒有這個nop會形成沒法在_start處設置斷點,爲gdb的bug,而我在kdbg中則是沒法擦看內存,斷點卻是能夠設置。因此仍是建議調試時,先增長這個nop指令。
上面的cpuid.s代碼範例使用軟中斷調用linux內核函數來實現打印和退出程序,還有另外的方法實現打印和退出,那就是調用C語言的標準庫函數,打印是printf,退出是exit。
假如CPUID=」GenuineIntel」,那麼可使用printf(「CPU ID is ‘%s’\n」,CPUID)來打印和cpuid.s同樣的輸出信息。下面就以printf爲例來描述彙編程序怎麼調用C庫函數。
調用子程序的指令是call,那麼調用printf就是用call printf,若是要實現printf(「CPU ID is ‘%s’\n」,CPUID),那裏面的兩個參數怎麼傳遞?這個要使用堆棧(stack),通常而言,C語言採用從右到左的順序入棧,即字符串CPUID的首地址先入棧,然後「CPU ID is ‘%s’\n」的首地址入棧。入棧的彙編代碼是pushl。所以,能夠按照以下的代碼實現printf(「CPU ID is ‘%s’\n」,CPUID)。
# 假設"CPU ID is '%s'\n"的首地址是output,讀回來的CPUID字符串首地址爲buffer
# 那麼如下代碼實現C庫函數調用:printf(output, buffer)
pushl $buffer
pushl $output
call printf
注意,C語言堆棧底部用高地址,pushl入棧後堆棧指針esp寄存器變小了4,所以,若是在call printf後,buffer和output再也不使用了,能夠把esp設置爲指向入棧前的地址,以便這兩個參數佔用的堆棧空間可使用。即pushl兩次後,esp減小了8,所以esp須要加上8才能回覆原來的堆棧位置,即addl $8, %esp。
這裏能夠簡單的理解:對於C庫函數func(param1, param2, …, paramn),調用的方法是先參數從右到左的順序入棧,而後call func。對於要深刻研究的話,提供abi接口文檔下載。
exit(ret)的調用方式輕易實現:
# exit(ret)的彙編調用代碼
pushl $ret
call exit
32位系統的abi和64位的abi(application binary interface)是不同的,所以x64的系統不能如此調用,因此在64位系統上運行32位的程序,必須按照32位的彙編和鏈接,不然會發生錯誤。具體可參考彙編語言程序設計讀書筆記(2)- 相關工具64位系統篇一文。
其實這個範例就是「3. 定義程序起始點」內容中的範例。理解了cpuid.s的意思,理解了怎麼調用C庫函數,那麼這個範例就能夠輕易的理解了。另外對於這個範例還需說明兩點。
.asiz: 由於printf打印的是0結尾的字符串,所以須要定義0結尾的字符串,因此用.asciz,而不用.ascii。
.lcomm: 聲明留出一塊本地內存,這裏.lcomm buffer, 12表示留出12個字節大小的一塊本地內存,首地址用buffer表示。
代碼很明瞭,先是讀取CPUID到ebx,edx,ecx寄存器,而後經過edi寄存器爲索引,把讀到的的字符串拼接到buffer中,而後分別以output和buffer爲參數調用printf,至關於調用了printf(「CPU ID is ‘%s’\n」, buffer)來打印,最後調用exit(0)返回。
經過本篇的描述,應該知道怎樣設計一個彙編程序,包括系統調用和C庫調用怎樣使用,最後在32位系統或64位系統上彙編鏈接運行。