VOOZH about

URL: https://qiita.com/drib__/items/cf4e31a05ccabc93a3ca

⇱ gasでx86アセンブラ(メモ) #プログラミング - Qiita


👁 Image
15

Go to list of users who liked

8

Share on X(Twitter)

Share on Facebook

Add to Hatena Bookmark

More than 3 years have passed since last update.

@drib__

gasでx86アセンブラ(メモ)

15
Last updated at Posted at 2018-12-07

GNU assembler(gas)でアセンブラを書く記事が少ないと思うので簡単にまとめてみました。
みなさんnasm、そしてintel記法が大好きだと思いますが今回はgas,AT&T記法を使用します。

環境

ld(リンカ)はGNU bfdのldを使用しています。

bash
$ 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.

ターミナルでプログラムの終了コードをすぐに確認できるように環境変数を設定しています。

.bashrc
PS1="\$?:[ssh]\W\u$ "
# (プログラムの返り値をすぐ確認できるように\$?を入れてます)

return 2 をする

ここでは、終了ステータスが2であるようなプログラムを作成します。

ret2.s
.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システムコールを呼んでやれば良さそうです。
👁 スクリーンショット 2021-10-09 13.38.27.png

exitシステムコールを呼ぶにはeaxに0x01、ebxがerror_code(プログラムの戻り値:今回は2)を指定して、int 0x80を実行します。

以下は、上のプログラムをコンパイルし実行した結果です。3行目でプログラムの戻り値が2であることが分かりますね!

bash
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ファイルを逆アセンブルしてみましょう。

bash
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ファイルのヘッダを見てみましょう。

bash
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");のようなプログラムを作成します。

hello.s
.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セクション)に文字列を置きました。

bash

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セクションに配置されていること

bash

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レジスタに代入しています。

別ファイルの関数を呼び出す

call.s
.text
.global _start
_start:
 call func#f.sのfuncの呼び出し
 mov $1,%eax
 mov $2,%ebx
 int $0x80
f.s
.text
.global func
func:
 mov $0x04,%eax
 mov $0x01,%ebx
 mov $msg,%ecx
 mov $0x07,%edx
 int $0x80
 ret
.data
msg: .asciz "hello\n"
bash
0:[ssh]test proxy$ gcc -m32 -nostdlib call.s f.s -o call
0:[ssh]test proxy$ ./call 
hello
2:[ssh]test proxy$ 

整理

syscall.inc
.equ sys_exit,1
.equ sys_write,4 
util.inc
.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 
run.s
.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のような感じです。
を参考にしました。

bash
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を入れてくれてそうです。(にもThe "z" in `.asciz' stands for "zero".と書いてありました。)。
これを利用することでWrite呼び出しの引数を2つから1つに減らせそうです。

Write改良

run.s
.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
util.inc
.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を参考にしました.図がわかりやすかったので是非見てみてください。

bash
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を参考にさせてもらいました。

run.s
.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
util.inc
.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の文字を出力する関数と改行する関数を整理し、これまでレジスタを関数の中で保存していなかったので保存するように変更しました。

bash
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$ 

感想

ファイルサイズが大きくなってきたので中断しました。
アセンブラは小さい部品を使用してプログラムを書いているという感じがして、楽しいです。

15

Go to list of users who liked

8
5

Go to list of comments

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15

Go to list of users who liked

8