目錄
前言
之前的文章有不少在討論執行檔該長怎麼樣。簡單來說,一個執行檔會有
- Sections:程式行為和資料會分開放在不同的sections
- 進入點,也就是system call開始執行你的程式的地方
以這樣的觀點,來看組合語言,會比較有感覺。
這次主要想要試看看如何使用組合語言印出Hello world。學過作業系統的朋友應該知道OS真正提供給使用者的介面叫作system call。有興趣的朋友可以使用strace
研究執行檔呼叫了那些system call。這次的Hello world我有兩個線索
- 在command line執行的process會有3個馬上可以使用的file descriptor(不知道那啥的請自行估狗)分別是
0
: standard in1
: standard out2
: standard error
- 有一個system call叫作
write
,你可以透過他把任何資料寫到指令的file descriptor
綜合以上,我們要幹的事就是透過組合語言做出
1
|
|
這又表示組合語言中我們要做
- 呼叫system call
- 帶參數給system call,這部份需要有
- ABI的背景知識
- 定址方式,更精確的說,如何宣告
"Hello world\n"
,讓runtime時放在在process address space中,並將它的位址傳給system call
測試環境
- Host
1 2 3 4 5 6 |
|
- Guest OS on Qemu
- 這邊很奇怪我的kernel用更新過的版本Qemu完全無法開機。目前裝死中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
範例:版本一
我本來想說慢慢來,先來個完全沒意義的r0 = 0; r1 = 1; r2 = r0 + r1。程式如下:
1 2 3 4 5 6 7 |
|
幾點說明:
$
或是#
代表一個數字(出處)%r1
代表ARM的r1
暫存器,但是為何用%
目前沒找到手冊上有說明。.text
前面的文章有看應該覺得很眼熟,就是告訴編譯器以下是程式行為。global
是讓symbol可以外露,白話來說就是nm
等binutil可以看的到這個symbol。_start
是一個程式執行的起始點,有看過之前文章就會覺得很眼熟。.end表示程式結束點,不過目前用起來有沒有加好像沒有差別。
Makefile
1 2 3 4 5 6 7 8 |
|
想法很簡單,就是直接編譯應該可以跑,雖然完全不會有畫面。錯!跑出來會這樣
1 2 3 4 5 6 7 8 |
|
這代表什麼,hello編譯完後的binary本身沒有執行權限。改改權限看可不可以跑?
1 2 3 |
|
怎麼回事?分析一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
看不出來對不對?我是這樣啦,所以先比對/bin/ls
的輸出吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
仔細看一下Type:
,/bin/ls
是EXEC
而hello
是REL
,man elf
可以看到REL
是relocate file,那是啥呢?根據System V Application Binary Interface - DRAFT - 10 June 2013第四章的簡介,簡單來說就是object檔案,也就是說link時吃的檔案。所以我們加入Link吧。
範例:版本二
單純加入linker看看會怎樣?
1 2 3 4 5 6 7 8 9 |
|
kerker,一樣GG。
1 2 3 4 5 |
|
估狗到的組合語言的Hello world範例最後會呼叫exit
system call,照著呼叫exit
就可以正常結束,這就是第三版。至於為何會出現Illegal instruction
,根據Scott Tsai大大的提示,當你的程式碼跑完後,接下來記憶體有啥CPU就跑啥,跑到不認識的opcode當然就GG了。
範例:版本三
1 2 3 4 5 6 7 |
|
單純叫了exit
而已,有幾點注意的
根據Debian ARM system call interface,可以知道
- r0 ~ r6是函數的帶入參數
- r7 是system call number,而system call number是啥呢?就是你要的system call 對應的數字。
所以要呼叫exit(0)
system 表示
- 傳一個參數,數值為
0
- 要設定
exit
對應的system call number為1
。
為何system call number是1
呢?可以看看unistd.h裏面system call number的定義,exit的system call number為1
。
設定完傳給exit
參數後呼叫了一個組合語言指令svc
,這個指令主要是切換到Supervisor模式。Linux下面也許太過複雜,不太容易從user mode一路追到kernel然後又看懂這些system call的行為。沒關係成大資工作業有比較看得懂的範例可以參考。例如包裝呼叫system call的函數以及Kernel中對應的system call服務實作。
根據unistd.h定義的system call number,write
的system call number 是4
,所以我們可以開始寫最後的版本了。
範例:版本四
開始之前,先來看男人怎麼介紹write system call的
1
|
|
這代表
- 有三個參數要傳給system call
- 有回傳值可以吃
- 其中一個參數是位址,這個位址我們會放
"Hello World\n"
字串
那麼我先來看看怎麼放字串到記憶體
1 2 3 |
|
.data
如果有看我以前的文章,就知道這是放有初始值全域變數的地方。
而hello_str
呢?嗯,對_start:
有印象嗎?
這叫作label,是GNU組語中symbol的一種,有興趣可以看這邊。根據手冊,label還有一個功能,代表目前跑到的位址。所以_start:
就是.text
section的起始位址。而hello_str
就是.data
section的起始位址。從這兩個label可以看到label只是一個位址,可以指向函數或是資料,這和C語言的指標有異曲同工之妙。有興趣的朋友可以去找function pointer和C語言的callback函數。
而.ascii
,單純就是宣告字串指令。
呼叫write
system call來還有兩個問題要處理
- 如何取得
hello_str
對應的位址放到暫存器r1
上面? - 要怎麼算出
hello_str
字串的長度?
關於第一個問題,GNU ARM組合語言有中將數值或位址放到暫存器的虛擬指令
1
|
|
expression是一種表示位置或是數值的方式。
恰巧symbol也算是一個expression,所以可以表示成:
1
|
|
第二個問題呢?要介紹.
了。之前看過linker script的朋友應該對於locale counter還有印象。locale counter代表目前的位置。加上expression
也支援運算。利用hello_str
是.data
開頭,我們可以這樣做:
1 2 3 4 |
|
綜合上面的討論,版本四組合語言會是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
等等,不是說有回傳值?還是要看Procedure Call Standard for the ARM Architecture
ABI r2.09手冊,上面提到回傳資料會放到r0
,剛好接下來的exit
system call帶的第一個參數也要存放在r0,那麼我們可以直接觀察執行後回傳值如下:
1 2 3 4 5 6 7 8 |
|
補充
1
|
|
這是個有趣的指令,這個指令事實上是個虛擬指令。怎麼說呢,因為這個指令的目標是把數值塞到指定的暫存器。這個數值是位址還是啥死人骨頭並不重要。重要的是,由於opcode的限制,把數值塞到指定的暫存器會有限制滴。例如ARM Cortex M0
的MOV
的數值只有8-bit,要塞32-bit的數值就需要配合其他的指令做連續技。因此
1
|
|
這樣的指令就可以讓你寫起來比較輕鬆。
另外一個值得一提是,如果組譯器無法把ldr <register> , = <expression>
虛擬指令轉換成MOV
或MVN
指令,把你的數值塞到暫存器的話。組譯器會把你的值放在一塊記憶體中,使用真的ldr把這塊記憶體的值載入到暫存器中。這個方法稱為literal pool,細節可以看這邊。
總結
本文從會GG的組合語言一路改到可以印出Hello World,並且在程式結束後回傳字串長度。在文章中簡單提到了GNU as的
- 組合語言中section
- 組合語言的編譯方式
- 組合語言中的symbol和字串表示方式
- 組合語言中的expression
- ABI實例
希望對有需要的朋友有所幫助。
延伸閱讀及致謝
感謝Scott Tsai大大提供的資料以及指出文章中錯誤的地方。另外他也有提到其他有趣的部份,當作以後的作業。先列出如下
- 從組合語言直接呼叫header file內的system call
- Kernel Memory Layout on ARM Linux
- 這邊主要討論的是
objdump -d
發現obj檔和執行檔差別只在進入點位址的差別,而進入點位址如何決定呢?這邊有規範,另外也可以看default linker script看看如何設定的。
- 這邊主要討論的是
- Scott大大的範例程式
- 可以看到,這個版本和我參考寫出來的版本差異有
- 把
"Hello world"
放在.rodata
section中,這比.data
更實際,因為這個字串的確沒有必要設成全域變數。 - 使用了preprocess方式。
- 提供了反組譯的結果
- 提供X86-64版本的組合語言比較
- 把
- 可以看到,這個版本和我參考寫出來的版本差異有
感謝Viler Hsiao大大寫文回答我的問題。
參考資料
- 『Hello World!』 in ARM assembly
- 本篇程式碼大量參考這篇。
- GNU Manual: Using as