読者です 読者をやめる 読者になる 読者になる

by shigemk2

当面は技術的なことしか書かない

池袋バイナリ勉強会 アセンブリ言語でhelloを書く #ikebin

UNIX

資料

7shi / ikebin / wiki / pdp11 / hello — Bitbucket

sys

hello worldですら長い

/ はコメント(Cで書いたことを想定している) writeっていう関数に引数をわたして実行するイメージ

/ write(1, hello, 6);
mov $1, r0
sys write
hello
6

/ exit(0);
mov $0, r0
sys exit

.data
hello: <hello\n>

writeはファイルに書き込む関数

  • 0が標準入力
  • 1が標準出力
  • 2が標準エラー出力

ファイルディスクリプタ

OSはファイルを数字で管理してて、それを直接みている write(1, hello, 6)で、 6は出力する文字数のこと。

ゆえに、標準出力でhello\nという文字を6文字出力する

  • 正常
  • 異常
main() {
  return 0; /* exit(1); */
}

exitしないとプログラムが暴走するので必ず書くこと。終了は明示的に書く

sys = システムコール命令…普通のプログラムと違い、OSが既に提供している

ユーザ→カーネルの呼び出しをシステムコールという。 OSが提供している特殊な関数=sys ユーザ定義の関数はsysではなくjsrで呼び出す

mov

moveの短縮形。

読み方も「むーぶ」

最初の引数はmovのところに書き、以降の引数はsysのあとに書く

こういう仕様のことをアプリケーションバイナリインターフェイス(ABI)という。 APIとは全く違う言葉。

mov $1, r0

そして、CPUやOSによってABIは全く違う。CPUが同じでもOSが違うとABIも変わってくる。

最近だと64ビットの場合はABIも統一するように勧められている。でもWinとLinuxとじゃ違う。

で、

mov $1, r0

って何?

ってことだが、

r0 = 1;

代入はこのように書く。代入が右向きであることに気をつけること。

movそのものはどのCPUにもあるが、その挙動はCPUによって異なる

なお、r0はレジスタ。

x86 Intel記法 vs AT&T記法

Intel

mov ax, 1
/ax = 1;

ふああああ 代入の方向が違う。

変数がデスティネーション 中身がソース

AT&T(PDPと同じ方向で代入する) gccが使っているのがこちらで、オープンソース系はおもにこちら

mov $1, %ax

cpuが同じなのに文法が違う

最初の引数はレジスタに入れて、あとの引数はシステムコールのあとに入れる

.data

バイナリはコードとデータが分けられている(例外はあるけど)

コードの部分のことはテキストといい、データの部分のことはデータという。

hello: <hello\n>
/ hello = "hello\n";
/変数名 vs 変数の中身

ゆえに、変数のなかみのほうを変えると表示されるものも変わる

ABI

mov $1, r0
sys
第二引数
第三引数

こういう決まりのことをABIという。

だけじゃなくて、 関数をよびだすときなどの決まりもある

Application Binary Interface - Wikipedia

V6のシステムコールの定義 http://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/sysent.c

文字列はr0に入れないABIの決まりが存在するし、しかも番号だけ登録されているものもあり、 廃止になったものは歯抜けになっていたりもするので実際のV6のシステムコールは40くらい

V6にいろいろ機能つけたしていったのが今のUNIXで、しかもV6のシステムコールはすべて今のUNIXで生きている

ただし、UNIX系の決まりなので、Windowsは知らない。iOSやAndroidもUNIX系がおおい。 ガラケーはUNIX系とは違うOSなのでまた違う。

うえのやつの実行

$ v6as write.s
$ v6strip a.out
$ 7run a.out
hello

逆アセンブル

逆アセンブルにおけるセミコロンはコメントアウトの意味合い。

$ 7run -d a.out
0000: 15c0 0001       mov $1, r0
0004: 8904            sys 4 ; write
0006: 0010            ; arg
0008: 0006            ; arg
000a: 15c0 0000       mov $0, r0
000e: 8901            sys 1 ; exit

8904 4番目のシステムコールがwriteで、1番目のシステムコールがexit 89がシステムコール

0010がhelloで、0006が6

バイナリ 8904で「システムコール4を呼び出す」

; argは「引数です」って話で、中身をダイレクトには表示しない。

データ領域は逆アセンブルしない

データはデータであって処理ではないから。テキスト領域の数字を解析するのが逆アセンブルの目的であるから。

  • 文字を数字に変換する(アセンブル)
  • 変換された数字を文字に戻す(逆アセンブル)

実際に逆アセンブルされた箇所は右側。慣れれば読める。

バイナリダンプ

なんでhelloが0010になるの?

バイナリは数字です!!

txtでもmp3でもjpegでも、コンピュータ上のすべてのファイルは数字のカタマリである。

ファイルの中身を数字で見る方法をバイナリダンプという。

$ hexdump -C a.out
00000000  07 01 10 00 06 00 00 00  0c 00 00 00 00 00 00 00  |................|
00000010  c0 15 01 00 04 89 10 00  06 00 c0 15 00 00 01 89  |................|
00000020  68 65 6c 6c 6f 0a 00 00  00 00 00 00 04 00 00 00  |hello...........|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 68 65 6c 6c  |............hell|
00000040  6f 00 00 00 03 00 10 00                           |o.......|
00000048
オフセット データ ASCII
00000000 07 01 10 00 06 00 00 00 0c 00 00 00 00 00 00 00 ................

データとASCIIは表示方法が違うだけで中身は同じ

hexl-mode(Emacs)

a.outをhexl-modeで起動したやつ。

f:id:shigemk2:20140201180718p:plain

$ hexdump -C a.out
00000000  07 01 10 00 06 00 00 00  0c 00 00 00 00 00 00 00  |................|
00000010  c0 15 01 00 04 89 10 00  06 00 c0 15 00 00 01 89  |................|
00000020  68 65 6c 6c 6f 0a 00 00  00 00 00 00 04 00 00 00  |hello...........|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 68 65 6c 6c  |............hell|
00000040  6f 00 00 00 03 00 10 00                           |o.......|
00000048
  • 最初の1行をヘッダ
  • 1行目がテキスト領域
  • 2行目がデータ領域

ファイルは

  • ヘッダ
  • text
  • data

の3つで構成されている

アスキーコード

  • 20はスペース
  • 30は0
  • 40はA
  • 61がa

ファイル→メモリ→テキストデータ

メモリの中をdumpしたもの。 ヘッダはメモリに配置されないので、ファイルのオフセットとメモリのアドレスはずれる。

hello 0010はメモリのアドレスのこと。

00000000  07 01 10 00 06 00 00 00  0c 00 00 00 00 00 00 00  |................|
00000010  c0 15 01 00 04 89 10 00  06 00 c0 15 00 00 01 89  |................|
00000020  68 65 6c 6c 6f 0a 00 00  00 00 00 00 04 00 00 00  |hello...........|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 68 65 6c 6c  |............hell|
00000040  6f 00 00 00 03 00 10 00                           |o.......|
00000048

バイナリエディタは16バイトごとに改行される。ので、上のやつはあくまで表示上のレイアウト。

たとえば、0-25は0x26バイト。さいごの数字はファイルのサイズ。

あくまで目的はプログラムをやりながら概念を理解していくこと。

コンパイラを作るときは関数型のほうが便利。でもHaskellはいろいろと面倒なので、 Cと同じような書き方が使えるF#を採用する。

束縛

関数型言語は後で値を変えると副作用が発生する。 あとで変数の値をかえることができないことを束縛という。

副作用を完全に排除していないF#を採用する。

ML→Caml→OCaml→F# (副作用を比較的に簡単に作れる。そしてOSを選ばずに開発ができる)

hexdump自作

fsharp

hexdump.fsx

let aout = System.IO.File.ReadAllBytes "../../a.out"
printfn "File Size = 0x%x" aout.Length
for i in 0 .. 16 .. aout.Length - 1 do
    printf "%04x " i
    for j in 0 .. 15 do
        if i + j < aout.Length then
            printf " %02x " aout.[i + j]
        else
            printf "    "
    for j in 0 .. 15 do
        if i + j < aout.Length then
            let n = int aout.[i + j]
            if 0x20 <= n && n <= 0x7e then
                printf "%c" (char n)
            else
            printf "."
    printfn ""
    

逆アセンブラ自作

$ 7run -d a.out
0000: 15c0 0001       mov $1, r0
0004: 8904            sys 4 ; write
0006: 0010            ; arg
0008: 0006            ; arg
000a: 15c0 0000       mov $0, r0
000e: 8901            sys 1 ; exit
$ hexdump -C a.out
00000000  07 01 10 00 06 00 00 00  00 00 00 00 00 00 01 00  |................|
00000010  c0 15 01 00 04 89 10 00  06 00 c0 15 00 00 01 89  |................|
00000020  68 65 6c 6c 6f 0a                                 |hello.|
00000026
  • hexdumpはリトルエンディアン
  • 逆アセンブラはビッグエンディアン(直感的)

フツウにつかうエンディアンはリトルエンディアン

なおかつ、リトルエンディアンは1バイトごとに並べられている

  • ビッグ 78563412
  • リトル 12 34 56 78

PDPの命令は4ケタ

101011→1010110(ビットシフト)

2a = a << 1
4a = a << 2
8a = a << 3
256a = a << 8

A or B

0x100a + b
= 256*a + b
= (a << 8) + b
= (a << 8) | b
00000000  07 01(ヘッダ) 10 00(textのサイズ) 06 00(dataのサイズ) 00 00  00 00 00 00 00 00 01 00  |................|
00000010  c0 15 01 00 04 89 10 00  06 00 c0 15 00 00 01 89  |................|
00000020  68 65 6c 6c 6f 0a                                 |hello.|
00000026

1バイト 2バイト = ワード 4バイト = ダブルワード(x86のときはあれだけど、ARMは4バイトでワードという。わけわかんねえ)

let aout = System.IO.File.ReadAllBytes "../../a.out"

let read16 (src:byte[]) index =
     (int src.[index]) ||| ((int src.[index + 1]) <<< 8)

let textsize = read16 aout 2
printfn "textsize = %d (0x%x)" textsize textsize
let text = aout.[0x10 .. 0x10 + textsize - 1]
let mutable i = 0
while i < text.Length do
    let w = read16 text i
    if w = 0x15c0 then
        let v = read16 text (i + 2)
        printfn "%04x %04x %04x mov $%x, r0" i w v v
        i <- i + 4
    else
        printfn "%04x %04x     ?" i w
        i <- i + 2

基本のスクリプトに、ちょっとず色々な命令を付け足していって、アセンブラのことを知っていく。 で、わからなかったら仕様書に頼る。