LoginSignup
15
7

More than 1 year has passed since last update.

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

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のような感じです。
ftp://ftp.gnu.org/old-gnu/Manuals/gas/html_chapter/as_7.html#SEC72を参考にしました。

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を入れてくれてそうです。(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改良

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

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
7