資料
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で起動したやつ。
$ 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
基本のスクリプトに、ちょっとず色々な命令を付け足していって、アセンブラのことを知っていく。 で、わからなかったら仕様書に頼る。