Hello Linux ARM 組合語言
目錄
- 前言
- 測試環境
- 範例:版本一
- 範例:版本二
- 範例:版本三
- 範例:版本四
- 總結
- 延伸閱讀及致謝
- 參考資料
前言
之前的文章有不少在討論執行檔該長怎麼樣。簡單來說,一個執行檔會有
- Sections:程式行為和資料會分開放在不同的sections
- 進入點,也就是system call開始執行你的程式的地方
以這樣的觀點,來看組合語言,會比較有感覺。
這次主要想要試看看如何使用組合語言印出Hello world。學過作業系統的朋友應該知道OS真正提供給使用者的介面叫作system call。有興趣的朋友可以使用strace研究執行檔呼叫了那些system call。這次的Hello world我有兩個線索
- 在command line執行的process會有3個馬上可以使用的file descriptor(不知道那啥的請自行估狗)分別是
- 0: standard in
- 1: standard out
- 2: standard error
- 有一個system call叫作write,你可以透過他把任何資料寫到指令的file descriptor
綜合以上,我們要幹的事就是透過組合語言做出
write(1, "Hello world\n", 12);
這又表示組合語言中我們要做
- 呼叫system call
- 帶參數給system call,這部份需要有
- ABI的背景知識
- 定址方式,更精確的說,如何宣告"Hello world\n",讓runtime時放在在process address space中,並將它的位址傳給system call
測試環境
Host
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.2 LTS Release: 14.04 Codename: trusty
Guest OS on Qemu
- 這邊很奇怪我的kernel用更新過的版本Qemu完全無法開機。目前裝死中。
$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 8.0 (jessie)
Release: 8.0
Codename: jessie
$ cat /proc/cpuinfo
Processor : ARM926EJ-S rev 5 (v5l)
BogoMIPS : 643.48
Features : swp half thumb fastmult vfp edsp java
CPU implementer : 0x41
CPU architecture: 5TEJ
CPU variant : 0x0
CPU part : 0x926
CPU revision : 5
Hardware : ARM-Versatile AB
Revision : 0000
Serial : 0000000000000000
範例:版本一
我本來想說慢慢來,先來個完全沒意義的r0 = 0; r1 = 1; r2 = r0 + r1。程式如下:
- hello.s
.text
.global _start
_start:
mov %r0, $0
mov %r1, $1
add %r2, %r0, %r1
.end
幾點說明:
- $或是#代表一個數字(出處)
- %r1代表ARM的r1暫存器,但是為何用%目前沒找到手冊上有說明。
- .text前面的文章有看應該覺得很眼熟,就是告訴編譯器以下是程式行為。
- global是讓symbol可以外露,白話來說就是nm等binutil可以看的到這個symbol。
- _start是一個程式執行的起始點,有看過之前文章就會覺得很眼熟。
.end表示程式結束點,不過目前用起來有沒有加好像沒有差別。
Makefile
TARGET=hello
AS_FILE=$(addsuffix .s, $(TARGET))
$(TARGET): $(AS_FILE)
$(AS) $^ -o $@
clean:
rm -rf $(TARGET)
想法很簡單,就是直接編譯應該可以跑,雖然完全不會有畫面。錯
!跑出來會這樣
$ make
as hello.s -o hello
$ ls -gG
total 12
-rw-r--r-- 1 588 Apr 20 18:04 hello
-rw-r--r-- 1 83 Apr 20 17:58 hello.s
-rw-r--r-- 1 113 Apr 20 17:58 Makefile
這代表什麼,hello編譯完後的binary本身沒有執行權限。改改權限看可不可以跑?
$ chmod +x hello
$ ./hello
bash: ./hello: cannot execute binary file: Exec format error
怎麼回事?分析一下
$ readelf hello -h
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: ARM
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 268 (bytes into file)
Flags: 0x5000000, Version5 EABI
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 8
Section header string table index: 5
看不出來對不對?我是這樣啦,所以先比對/bin/ls的輸出吧
$ readelf /bin/ls -h
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x14354
Start of program headers: 52 (bytes into file)
Start of section headers: 99372 (bytes into file)
Flags: 0x5000202, has entry point, Version5 EABI, soft-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 27
Section header string table index: 26
仔細看一下Type:,/bin/ls是EXEC而hello是REL,man elf可以看到REL是relocate file,那是啥呢?根據System V Application Binary Interface - DRAFT - 10 June 2013第四章的簡介,簡單來說就是object檔案,也就是說link時吃的檔案。所以我們加入Link吧。
範例:版本二
單純加入linker看看會怎樣?
TARGET=hello
AS_FILE=$(addsuffix .s, $(TARGET))
$(TARGET): $(AS_FILE)
$(AS) $^ -o $@.o
$(LD) $@.o -o $@
clean:
rm -rf $(TARGET)
kerker,一樣GG。
$ make
as hello.s -o hello.o
ld hello.o -o hello
$ ./hello
Illegal instruction 估狗到的組合語言的Hello world範例最後會呼叫exit system call,照著呼叫exit就可以正常結束,這就是第三版。至於為何會出現Illegal instruction,根據Scott Tsai大大的提示,當你的程式碼跑完後,接下來記憶體有啥CPU就跑啥,跑到不認識的opcode當然就GG了。
範例:版本三
.text
.global _start
_start:
mov %r0, $0
mov %r7, $1
svc $0
.end
單純叫了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的
ssize_t write(int fd, const void *buf, size_t count);
這代表
- 有三個參數要傳給system call
- 有回傳值可以吃
- 其中一個參數是位址,這個位址我們會放"Hello World\n"字串 那麼我先來看看怎麼放字串到記憶體
.data
hello_str:
.ascii "Hello World\n"
.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組合語言有中將數值或位址放到暫存器的虛擬指令
ldr <register> , = <expression>
expression是一種表示位置或是數值的方式。
恰巧symbol也算是一個expression,所以可以表示成:
ldr %r1, =hello_str
第二個問題呢?要介紹.了。之前看過linker script的朋友應該對於locale counter還有印象。locale counter代表目前的位置。加上expression也支援運算。利用hello_str是.data 開頭,我們可以這樣做:
.data
hello_str:
ascii "Hello World\n"
hello_len = . - hello_str
綜合上面的討論,版本四組合語言會是
.data
hello_str:
.ascii "Hello World\n"
hello_len = . - hello_str
.text
.global _start
_start:
/* %r0 = write(1, hello_str, hello_len); */
mov %r0, $1
ldr %r1, =hello_str
ldr %r2, =hello_len
mov %r7, $4
svc $0
/* exit(%r0) */
mov %r7, $1
svc $0
.end
等等,不是說有回傳值?還是要看Procedure Call Standard for the ARM Architecture ABI r2.09手冊,上面提到回傳資料會放到r0,剛好接下來的exit system call帶的第一個參數也要存放在r0,那麼我們可以直接觀察執行後回傳值如下:
$ make
as hello.s -o hello.o
ld hello.o -o hello
$ ./hello
Hello World
$ echo $?
12
補充
ldr <register> , = <expression>
這是個有趣的指令,這個指令事實上是個虛擬指令。怎麼說呢,因為這個指令的目標是把數值塞到指定的暫存器
。這個數值是位址還是啥死人骨頭並不重要。重要的是,由於opcode的限制,把數值塞到指定的暫存器會有限制滴。例如ARM Cortex M0的MOV的數值只有8-bit,要塞32-bit的數值就需要配合其他的指令做連續技。因此
ldr <register> , = <expression>
這樣的指令就可以讓你寫起來比較輕鬆。
另外一個值得一提是,如果組譯器無法把ldr 真
的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版本的組合語言比較
- 可以看到,這個版本和我參考寫出來的版本差異有
hello world in assembler under x86-64 and ARMv7-A Linux
- hello-arm.lst
$ gdbdis -r hello-arm _start
0x00010098 <+0>: 04 27 movs r7, #4
0x0001009a <+2>: 01 20 movs r0, #1
0x0001009c <+4>: 03 49 ldr r1, [pc, #12] ; (0x100ac <_start+20>)
0x0001009e <+6>: 5f f0 0d 02 movs.w r2, #13
0x000100a2 <+10>: 00 df svc 0
0x000100a4 <+12>: f8 27 movs r7, #248 ; 0xf8
0x000100a6 <+14>: 00 20 movs r0, #0
0x000100a8 <+16>: 00 df svc 0
0x000100aa <+18>: 00 00 movs r0, r0
0x000100ac <+20>: b0 00 01 00 strheq r0, [r1], -r0 ; <UNPREDICTABLE>
- hello-arm.S
.syntax unified
.thumb
#include <sys/syscall.h>
.section .text
.global _start
.thumb_func
_start:
/* write(1, "hello, world", 13) */
movs r7, SYS_write
movs r0, 1
ldr r1, =hello_str
movs r2, hello_str_len
svc 0
/* exit_group(0) */
movs r7, SYS_exit_group
movs r0, 0
svc 0
.section .rodata
hello_str:
.ascii "hello, world\n"
.set hello_str_len, . - hello_str
- hello-x86-64.lst
$ gdbdis -r hello-x86-64 _start
0x00000000004000d4 <+0>: 48 c7 c0 01 00 00 00 mov $0x1,%rax
0x00000000004000db <+7>: 48 c7 c7 01 00 00 00 mov $0x1,%rdi
0x00000000004000e2 <+14>: 48 c7 c6 02 01 40 00 mov $0x400102,%rsi
0x00000000004000e9 <+21>: 48 c7 c2 0d 00 00 00 mov $0xd,%rdx
0x00000000004000f0 <+28>: 0f 05 syscall
0x00000000004000f2 <+30>: 48 c7 c0 e7 00 00 00 mov $0xe7,%rax
0x00000000004000f9 <+37>: 48 c7 c7 00 00 00 00 mov $0x0,%rdi
0x0000000000400100 <+44>: 0f 05 syscall
- hello-x86-64.S
#include <sys/syscall.h>
.section .text
.global _start
_start:
/* write(1, "hello, world\n", 13) */
mov $SYS_write, %rax
mov $1, %rdi
mov $hello_str, %rsi
mov $hello_str_len, %rdx
syscall
/* exit_group(0) */
mov $SYS_exit_group, %rax
mov $0, %rdi
syscall
.section .rodata
hello_str:
.ascii "hello, world\n"
.set hello_str_len, . - hello_str
參考資料
『Hello World!』 in ARM assembly 本篇程式碼大量參考這篇。 GNU Manual: Using as