高級語言教程從Hello world程序開始是慣例祖乳,但匯編語言不太一樣,Hello world程序也需要更多知識才能寫出,通常在整個教程的三分之一之后才會講。這個教程我會把Hello world程序放在比較靠前的位置考赛,作為第二個匯編程序。第一個程序在上一篇教程出現(xiàn)莉测,運行后沒有任何輸出颜骤。我們現(xiàn)在回顧一下:
global _main
_main:
mov rax, 0
ret
我會先講一些預備知識,再解釋這段程序捣卤,再之后開始我們的Hello world程序忍抽。
預備知識——C和匯編程序的產(chǎn)生過程
生成過程
一個C語言源程序文件八孝,需要經(jīng)過兩步才能轉換成可執(zhí)行文件。
- 使用C編譯器對源文件進行編譯鸠项,生成“目標文件”
- 使用“鏈接器”對一個或多個目標文件進行鏈接唆阿,生成可執(zhí)行文件
匯編語言同樣是兩個步驟:
- 使用匯編器對源文件進行匯編,生成“目標文件”
- 使用“鏈接器”對一個或多個目標文件進行鏈接锈锤,生成可執(zhí)行文件
除了第一步兩處斜體字不同外,都是一樣的闲询。其實過程是相同的久免,只是術語不同,“編譯”是針對高級語言的扭弧,所用的工具是“編譯器”阎姥,“匯編”是針對匯編語言的,使用的工具是“匯編器”鸽捻,我們使用的nasm是一種匯編器呼巴。事實上,你按高級語言的叫法把“匯編”和“匯編器”叫做“編譯”和“編譯器”也沒什么不妥御蒲,大家也能聽得懂衣赶。
C程序生成
我們開始實戰(zhàn)C語言的程序生成,我們有C語言的Hello world程hello.c厚满,如下
#include <stdio.h>
int main() {
printf("Hello world\n");
return 0;
}
第一步編譯:
在終端輸入命令
gcc -c hello.c
便會在當前目錄下生成目標文件hello.o
府瞄。
第二步:鏈接
我們接著輸入命令
gcc hello.o
便會在當前目錄下生成可執(zhí)行文件a.out
。
輸入
./a.out
就可以看到顯示出了Hello world碘箍,默認的可執(zhí)行文件名a.out有點奇怪遵馆,我們可加上-o
參數(shù),指定生成的文件名
gcc hello.o -o hello
就生成了名為hello的可執(zhí)行文件丰榴。
我們的編譯器和鏈接器都是gcc货邓,這種簡單的程序通常我們把兩步合成一步
gcc hello.c -o hello
這樣會生成可執(zhí)行文件hello,不會生成目標文件hello.o
四濒。
匯編程序生成
之前寫好的hello.s
文件
第一步:匯編
nasm -f macho64 test.s
這個命令會生成目標文件test.o
-f macho64
表示生成macOS平臺x86_64格式的目標文件换况。如果你用-f macho
會生成32位的目標文件,雖然可以完成匯編峻黍,但你在鏈接的時候會出錯复隆,因為macOS只支持64位程序, 無法鏈接32位的目標文件姆涩,這也是系統(tǒng)自帶的nasm匯編器不能用的原因挽拂。
第二步:鏈接
gcc test.o -o test
這就會生成可執(zhí)行文件test。
這里的工具仍是gcc骨饿,命令看起來也一樣亏栈,看起和C語言的鏈接過程什么區(qū)別台腥。是的,沒有區(qū)別绒北,我們用的是一個工具黎侈。
gcc在對目標文件鏈接的時候,并不知道目標文件是什么語言生成的闷游,可能是匯編峻汉,也可能是C、Go脐往、D等高級語言休吠。通過這種方式可以很容易混合匯編和高級語言編程。后邊的教程就會有匯編和C一起工作的例子业簿。
執(zhí)行程序
我們的test程序執(zhí)行后雖然看不到輸出瘤礁,但是這個程序是有返回值的。
我們執(zhí)行
./test
后梅尤,接著輸入命令
echo $柜思?
我們看到顯示0,$?
表示上一條命令的返回值巷燥,在類Unix系統(tǒng)赡盘,程序返回0表示成功,1到255表示程序失敗矾湃。
你也可兩條命令寫在一行
./test ; echo $?
分號是命令分隔符亡脑。
第一個匯編程序解釋
global _main
表示程序入口是_main:
處,你也可以把_main
修改成別的名字邀跃,改后程序鏈接的時候要指明入口霉咨,要麻煩一些,最好不要改拍屑。
mov rax, 0
表示把0放入rax寄存器途戒,寄存器你可以簡單理解為在CPU內(nèi)部的超高速內(nèi)存。CPU有多個寄存器僵驰,rax是一個寄存器的名稱喷斋,下一篇教程會講蒜茴。
ret
表示返回
你可以試著把程序里的0改成1或者260,重新運行一下粉私,并查看$?
的值,看看是什么結果诺核。
回顧一下我們學過的兩條指令
mov
指令抄肖,把數(shù)據(jù)放入寄存器中
ret
指令久信,返回
Hello world程序
開始匯編語言的Hello world之前漓摩,先寫一個C語言的Hello world,之后再轉化成匯編管毙,hello2.c如下:
#include <unistd.h>
int main() {
char *msg = "Hello world\n"; // 定義要輸出的文字msg
write(1, msg, 12); // 輸出msg腿椎,12為msg的長度
_exit(0); // 調用_exit函數(shù)返回
}
等等,之前不是寫過了嗎夭咬,為啥又寫一個?之前的hello.c轉化成匯編有點麻煩皱埠,所以要另寫一個咖驮。
寫一個等價的匯編程序hello1.s
msg: db "Hello World", 0x0a
global _main
_main:
mov rax, 0x2000004
mov rdi, 1
mov rsi, msg
mov rdx, 12
syscall
ret
mov rax, 0x2000001
mov rdi, 0
syscall
ret
匯編并鏈接
nasm -f macho64 hello1.s && gcc hello1.o -o hello1
&&
的作用是連接多條命令,但某一條命令失斖伞(返回值不為0)睦刃,就不再執(zhí)行后面的命令砚嘴。和之前提到的分號(;
)不同涩拙,分號不管成功與否都會依次執(zhí)行命令。
會出現(xiàn)警告:
ld: warning: PIE disabled. Absolute addressing (perhaps -mdynamic-no-pic) not allowed in code signed PIE, but used in _main from hello1.o. To fix this warning, don't compile with -mdynamic-no-pic or link with -Wl,-no_pie
先不管工育,接著運行./hello1
可以看到能正常輸出Hello world搓彻。
程序的解釋我先以注釋形式放在程序內(nèi),匯編的注釋以分號開頭旭贬,到行末結束。
; 定義要輸出的文字msg稀轨,db是data byte的意思
; 0x0a表示換行符,0x前綴表示十六進制谎势,
; 也可以用h后綴表示十六進制,比如41h脏榆,0ch轩触,以a~f開頭的十六進制前面一定要加0
msg: db "Hello World", 0x0a
global _main
_main:
; 要調用的write函數(shù),放入寄存器rax
mov rax, 0x2000004
mov rdi, 1 ; 第1個參數(shù)1坞生,放入寄存器rdi
mov rsi, msg ; 第2個參數(shù)msg,放入寄存器rsi是己,此行鏈接時會報警告
mov rdx, 12 ; 第3個參數(shù)12,放入寄存器rdx
syscall ; 調用rax寄存器中的函數(shù)
ret ; 函數(shù)調用返回
; 要調用的_exit函數(shù)卒废,放入寄存器rax
mov rax, 0x2000001
mov rdi, 0 ; 第1個參數(shù)0,放入寄存器rdi
syscall ; 調用rax寄存器中的函數(shù)
ret ; 函數(shù)調用返回
從注釋中可以看到
要調用的函數(shù)需要放入寄存器rax
中逆皮,參數(shù)要依次放入寄存器rdi
参袱,rsi
,rdx
中抹蚀。
我們修復一下警告,并重構一下代碼
警告是由指令mov rsi, msg
引起的牢贸。
這條指令的意思是把msg
的地址放到寄存器rsi
中镐捧,而鏈接器認為你使用了絕對地址,不能直接使用msg的地址懂酱。
我們用lea rsi [rel msg]
替換剛才的語句就可以了。
12是Hello World字符串的長度整陌,改了字符串還要改這個值,可以自動計算字符串的長度
兩段函數(shù)調用處都有泌辫,syscall
和ret
語句,這兩句是調用系統(tǒng)內(nèi)核宾毒,可以提取一個公用的代碼段
修復了這三個問題的程序hello2.s如下:
SECTION .data ; 數(shù)據(jù)代碼段
msg: db "Hello World", 0x0a
len: equ $-msg ; 計算msg的長度殿遂,賦值給len
SECTION .text ; 程序代碼段
global _main
kernal:
syscall
ret
_main:
mov rax, 0x2000004
mov rdi, 1
lea rsi, [rel msg]
mov rdx, len ; 把len的值作為參數(shù)傳入
call kernal ; 調用kernal處的代碼
mov rax, 0x2000001
mov rdi, 0
call kernal
這段hello2.s程序修復了上面所說的3個問題,并添加了SECTION .data
和SECTION .text
代碼段說明墨礁,使程序看起來更明了,.data
和.text
代碼段的名稱是匯編器定義的焕毫,不能更改驶乾。
好了,運行正常轻掩,格式規(guī)范的Hello world程序1.0版就完成了懦底。匯編語言的Hello world這么復雜!你一定有些不明白的地方吧聚唐,比如lea
和mov
指令有什么區(qū)別,rax中存放的數(shù)據(jù)是什么意思扮惦?隨著教程的繼續(xù)亲桦,這些問題會逐漸明朗起來的。