GNU assembler(gas)でアセンブラを書く記事が少ないと思うので簡単にまとめてみました。
みなさんnasm、そしてintel記法が大好きだと思いますが今回は宗教上の理由によりgas,AT&T記法を使用します。
#環境
ld(リンカ)はGNU bfdのldを使用しています。
$ uname -i -s -o -p -r
Linux 5.11.0-37-generic x86_64 x86_64 GNU/Linux
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ ld --version
GNU ld (GNU Binutils for Ubuntu) 2.34
Copyright (C) 2020 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.
ターミナルでプログラムの終了コードをすぐに確認できるように環境変数を設定しています。
PS1="\$?:[ssh]\W \u$ "
#(プログラムの返り値をすぐ確認できるように\$?を入れてます)
#return 2 をする
ここでは、終了ステータスが2であるようなプログラムを作成します。
.text
.global _start
_start:
mov $1,%eax #systemcallの番号を指定
mov $2,%ebx #引数(今回は戻り値)
int $0x80
1行目の.textはそれ以下がtextセクション(実行命令が保存される場所)であることを指定します
2行目の.global _startはリンカに_startが存在することを知らせています。(デフォルトのgnuのリンカスクリプトは_startからプログラムが実行されるようにELFファイルが生成されます。リンカスクリプトを作成することで任意の関数から実行することもできます。)
3行目から_start関数の内容です。
Linux Syscall Tableを参考にします。今回は戻り値を指定してプログラムを終了するだけなのでexitシステムコールで良さそうです。
return 2;つまり終了ステータスが2であるようなプログラムを作成するためには、引数に2を指定してexitシステムコールを呼んでやれば良さそうです。
exitシステムコールを呼ぶにはeaxに0x01、ebxがerror_code(プログラムの戻り値:今回は2)を指定して、int 0x80を実行します。
以下は、上のプログラムをコンパイルし実行した結果です。3行目でプログラムの戻り値が2であることが分かりますね!
0:[ssh]test proxy$ gcc -m32 -nostdlib ret2.s -o ret2
0:[ssh]test proxy$ ./ret2
2:[ssh]test proxy$
コンパイルは-nostdlibオプションを指定することで標準ライブラリをリンクしないようにしています。
(ただしgnuの標準ライブラリ(libc)を使用しないのでgnuのライブラリの恩恵を受けられなくなります。その代わりlibcを含まないとファイルサイズが小さくなるなどというメリットがあります)
-m32と指定することでx86のELFを生成しています。
生成されたELFファイルの検証
_start関数が.textに配置されていること
生成されたELFファイルを逆アセンブルしてみましょう。
0:[ssh]test proxy$ objdump -d ./ret2
./ret2: file format elf32-i386
Disassembly of section .text:
00001000 <_start>:
1000: b8 01 00 00 00 mov $0x1,%eax
1005: bb 02 00 00 00 mov $0x2,%ebx
100a: cd 80 int $0x80
_start関数が.textセクションに配置されていることが分かりますね!
_start関数の命令列の逆アセンブル結果はret.sの内容と同じことも確かめられますね。
ELFファイルの実行が_start関数から始まること
ELFファイルのヘッダを見てみましょう。
0:[ssh]test proxy$ readelf -h ./ret2
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: DYN (Shared object file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x1000
Start of program headers: 52 (bytes into file)
Start of section headers: 12652 (bytes into file)
Flags: 0x0
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: 12
Section header string table index: 11
Entry point addressが0x1000となってますね。
0x1000とはどこでしょうか?一つ前の逆アセンブル結果を見ると、_start関数が0x1000から始まってますね。つまりELFファイルの実行は_start関数から始まることが確かめられました!
#"hello"という文字列を出力する
c言語で言うとputs("hello");のようなプログラムを作成します。
.data
msg:.asciz "hello\n"
.text
.global _start
_start:
mov $0x04,%eax #sys_write
mov $0x01,%ebx #fd:今回はstdout
mov $msg,%ecx #文字列のアドレス
mov $0x06,%edx #文字列の長さ
int $0x80
mov $0x1,%eax
mov $0x2,%ebx
int $0x80
今回はhelloを標準出力させていのでwriteシステムコールを使用します。
Linux Syscall Tableを参考にするとecxには文字列のあるアドレスを指定する必要があるので.data(dataセクション)に文字列を置きました。
0:[ssh]test proxy$ gcc -m32 -nostdlib write.s -o write
0:[ssh]test proxy$ ./write
hello
2:[ssh]test proxy$
生成されたELFファイルの検証
文字列"hello\n"が.dataセクションに配置されていること
0:[ssh]test proxy$ objdump -d -s ./write
./write: file format elf32-i386
Contents of section .interp:
0154 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so
0164 2e3200 .2.
Contents of section .note.gnu.build-id:
0168 04000000 14000000 03000000 474e5500 ............GNU.
0178 a3af4499 40fa7f74 fd3648da aba35b7a ..D.@..t.6H...[z
0188 54b64e88 T.N.
Contents of section .gnu.hash:
018c 01000000 01000000 01000000 00000000 ................
019c 00000000 00000000 ........
Contents of section .dynsym:
01a4 00000000 00000000 00000000 00000000 ................
Contents of section .dynstr:
01b4 00 .
Contents of section .rel.dyn:
01b8 0b100000 08000000 ........
Contents of section .text:
1000 b8040000 00bb0100 0000b900 300000ba ............0...
1010 06000000 cd80b801 000000bb 02000000 ................
1020 cd80 ..
Contents of section .dynamic:
2f70 f5feff6f 8c010000 05000000 b4010000 ...o............
2f80 06000000 a4010000 0a000000 01000000 ................
2f90 0b000000 10000000 15000000 00000000 ................
2fa0 11000000 b8010000 12000000 08000000 ................
2fb0 13000000 08000000 16000000 00000000 ................
2fc0 1e000000 0c000000 fbffff6f 01000008 ...........o....
2fd0 faffff6f 01000000 00000000 00000000 ...o............
2fe0 00000000 00000000 00000000 00000000 ................
2ff0 00000000 00000000 00000000 00000000 ................
Contents of section .data:
3000 68656c6c 6f0a00 hello..
Disassembly of section .text:
00001000 <_start>:
1000: b8 04 00 00 00 mov $0x4,%eax
1005: bb 01 00 00 00 mov $0x1,%ebx
100a: b9 00 30 00 00 mov $0x3000,%ecx
100f: ba 06 00 00 00 mov $0x6,%edx
1014: cd 80 int $0x80
1016: b8 01 00 00 00 mov $0x1,%eax
101b: bb 02 00 00 00 mov $0x2,%ebx
1020: cd 80 int $0x80
ELFファイルを逆アセンブルしてみましょう。
0x3000から.dataセクションが始まっていて"hello\n"が配置されていることが分かりますね。
_start関数の0x100aでは0x3000(.dataセクションの"hello\n"のアドレス)をecxレジスタに代入しています。
#別ファイルの関数を呼び出す
.text
.global _start
_start:
call func#f.sのfuncの呼び出し
mov $1,%eax
mov $2,%ebx
int $0x80
.text
.global func
func:
mov $0x04,%eax
mov $0x01,%ebx
mov $msg,%ecx
mov $0x07,%edx
int $0x80
ret
.data
msg: .asciz "hello\n"
0:[ssh]test proxy$ gcc -m32 -nostdlib call.s f.s -o call
0:[ssh]test proxy$ ./call
hello
2:[ssh]test proxy$
#整理
.equ sys_exit,1
.equ sys_write,4
.include "syscall.inc"
Exit:#プログラムを終了
mov %eax,%ebx
mov $sys_exit,%eax
int $0x80
Write:
mov %eax,%ecx
mov $sys_write,%eax
mov %ebx,%edx
mov $0x1,%ebx
int $0x80
ret
.include "util.inc"
.text
.global _start
_start:
mov $msg,%eax
mov $len,%ebx
call Write
mov $len,%eax
call Exit
.data
msg: .asciz "hello world!\n"
.equ len, .-msg
少し長くなりましたがやっていることはrun.sでWrite呼び出しをしてmsgのサイズを引数にExit呼び出しをしているだけです。
.equ,.includeはc言語でいうと#define,#includeのような感じです。
ftp://ftp.gnu.org/old-gnu/Manuals/gas/html_chapter/as_7.html#SEC72を参考にしました。
0:[ssh]test proxy$ gcc -m32 -nostdlib run.s -o run
0:[ssh]test proxy$ ./run
hello world!
14:[ssh]test proxy$
出力結果を見るとhello world!と返り値が表示されています。
返り値に注目すると、run.sファイルの"hello world!\n"は13byteに対して14という値が帰ってきています。おそらく.ascizは文字列の最後に\0を入れてくれてそうです。(ftp://ftp.gnu.org/old-gnu/Manuals/gas/html_chapter/as_7.html#SEC72にもThe "z" in `.asciz' stands for "zero".と書いてありました。)。
これを利用することでWrite呼び出しの引数を2つから1つに減らせそうです。
#Write改良
.include "util.inc"
.text
.global _start
_start:
mov $msg,%eax
call Write
call Exit #Writeはeaxに文字列の長さを返すのでそのままExitを呼ぶ
.data
msg: .asciz "ok\n"
.equ len, .-msg
.include "syscall.inc"
Exit:#プログラムを終了
mov %eax,%ebx
mov $sys_exit,%eax
int $0x80
Write:#文字列を出力
mov %eax,%ecx
call Strlen
mov %eax,%edx
mov $sys_write,%eax
mov $0x1,%ebx
int $0x80
ret
Strlen:#文字列の長さを返す
mov %eax,%edi
push %eax
xor %eax,%eax
mov $0xFFFF,%ecx
repne scasb
pop %ecx
sub %ecx,%edi
mov %edi,%eax
ret
Writeを改良したことによりeaxレジスタだけでWrite呼び出しができるようになりました。
Write呼び出しの中でStrlenを呼び出して文字列の長さを求めています。Strlenのはhttp://softwaretechnique.jp/OS_Development/Tips/IA32_Instructions/SCAS.htmlを参考にしました.図がわかりやすかったので是非見てみてください。
0:[ssh]test proxy$ gcc -m32 -nostdlib run.s -o run
0:[ssh]test proxy$ ./run
ok
4:[ssh]test proxy$
#コマンドライン引数からhelloする
c言語ではコマンドライン引数にはargc,とargvでアクセスできるのでアセンブラでもアクセスする方法があるはずです。
https://www.mztn.org/lxasm/asm06.htmlを参考にさせてもらいました。
.include "util.inc"
.text
.global _start
_start:
pop %ebx#argc
xor %ecx,%ecx
pop %eax#argv[0]
inc %ecx
call Write
call Newline
cmp %ecx,%ebx
je end
loop:
pop %eax
inc %ecx
call Write
call Newline
cmp %ecx,%ebx
jne loop
end:
xor %eax,%eax
call Exit
.include "syscall.inc"
Exit:#プログラムを終了
mov %eax,%ebx
mov $sys_exit,%eax
int $0x80
Write:#文字列を出力
push %ecx
push %ebx
mov %eax,%ecx
call Strlen
mov %eax,%edx
mov $sys_write,%eax
mov $0x1,%ebx
int $0x80
pop %ebx
pop %ecx
ret
Strlen:#文字列の長さを返す
mov %eax,%edi
push %eax
xor %eax,%eax
mov $0xFFFF,%ecx
repne scasb
pop %ecx
sub %ecx,%edi
mov %edi,%eax
ret
Writechar:#1byte出力
push %ebx
push %ecx
push %eax
mov $sys_write,%eax
mov $1,%ebx
mov $1,%edx
mov %esp,%ecx
int $0x80
pop %eax
pop %ecx
pop %ebx
ret
Newline:#改行
mov $0x0a,%al
call Writechar
ret
変更点としてはutil.incに1byteの文字を出力する関数と改行する関数を整理し、これまでレジスタを関数の中で保存していなかったので保存するように変更しました。
0:[ssh]test proxy$ gcc -m32 -nostdlib run.s -o run
0:[ssh]test proxy$ ./run hoge huga
./run
hoge
huga
0:[ssh]test proxy$
#感想
ファイルサイズが大きくなってきたので中断しました。
アセンブラは小さい部品を使用してプログラムを書いているという感じがして、楽しいです。