定義
オブジェクトファイル (object file) またはオブジェクトコード (object code) とは、コンパイラがソースコードを処理した結果生成される中間的なコード表現である。オブジェクトファイルの内容はバイナリであり、コンパクトで構文解析済みのコードである。
内容
cのファイルをコンパイルしたら出てくるoファイルがそれ。
オブジェクトファイル (object file) またはオブジェクトコード (object code) とは、コンパイラがソースコードを処理した結果生成される中間的なコード表現である。オブジェクトファイルの内容はバイナリであり、コンパクトで構文解析済みのコードである。
cのファイルをコンパイルしたら出てくるoファイルがそれ。
使うPythonをかえて、 brew install mercurialではなくeasy_install mercurialしたらうまくいった。
7shi / ikebin / wiki / pdp11 — Bitbucket
40年前のUNIX v6 1975
これ以上簡単なものになってしまうと実用性が皆無になる
自分でコンパイラを作ってOSを作ろうとしたけど、 アセンブラって何?ってところから始まってしまった
コンパイラはOSの上で動くので、OSのことが分かってないとコンパイラのことも分からないし、作れない
題材をだしてやっていく。
v6ccで昔のバイナリを呼び出す
UNIX V6 OS(40年前) PDP-11 CPU(40年前)
V6を8086(x86の初期)に移植する作業をやってる
CPUの命令やバイナリの仕組みを覚えておいて損はない (ただしオペランド(引数)の細かいところには大きな違いがある)
右側の命令群がアセンブリ言語で、命令群を数字に変換するのがアセンブル。
C→アセンブリ→バイナリ Cからアセンブリに変換するのをコンパイル、 アセンブルからバイナリに変更することをアセンブルという。
逆にバイナリからアセンブリに変換することを逆アセンブルという。
[crt0.o] start: 0000: f009 setd 0002: 1180 mov sp, r0 0004: 1226 mov (r0), -(sp) 0006: 0bd0 tst (r0)+ 0008: 1036 0002 mov r0, 2(sp) 000c: 09f7 0008 jsr pc, 0018 ; _main 0010: 100e mov r0, (sp) 0012: 09df 024e jsr pc, *$024e ; _exit 0016: 8901 sys 1 ; exit
上に書いてあるアセンブリはPDP11のものなので、CPUが違うと命令の書き方も変わる。
CPUのなかには変数があって、その変数をレジスタとよんで差し支えない。
フラグはレジスタとはまた別。
実際に実行しながら表示しているので、メモリに数字が格納されてる。
$ 7run -m a.out r0 r1 r2 r3 r4 r5 sp flags pc start: 0000 0000 0000 0000 0000 0000 fff6 ---- 0000:f009 setd 0000 0000 0000 0000 0000 0000 fff6 ---- 0002:1180 mov sp, r0 fff6 0000 0000 0000 0000 0000 fff6 -N-- 0004:1226 mov (r0), -(sp) ;[fff6]0001 ;[fff4]0000 fff6 0000 0000 0000 0000 0000 fff4 ---- 0006:0bd0 tst (r0)+ ;[fff6]0001 fff8 0000 0000 0000 0000 0000 fff4 ---- 0008:1036 0002 mov r0, 2(sp) ;[fff6]0001 fff8 0000 0000 0000 0000 0000 fff4 -N-- 000c:09f7 0008 jsr pc, 0018 ;[0018]0977 _main: fff8 0000 0000 0000 0000 0000 fff2 -N-- 0018:0977 023c jsr r5, 0258 ;[0258]1140 csv: fff8 0000 0000 0000 0000 001c fff0 -N-- 0258:1140 mov r5, r0 001c 0000 0000 0000 0000 001c fff0 ---- 025a:1185 mov sp, r5 001c 0000 0000 0000 0000 fff0 fff0 -N-- 025c:1126 mov r4, -(sp) ;[ffee]0000 001c 0000 0000 0000 0000 fff0 ffee Z--- 025e:10e6 mov r3, -(sp) ;[ffec]0000 001c 0000 0000 0000 0000 fff0 ffec Z--- 0260:10a6 mov r2, -(sp) ;[ffea]0000
変数の呼び出しがかかるとメモリの間を行ったりきたりしている
逆アセンブルはただ解析しているだけ。
CPUがアップデートすると、言語のコンパイラもそれにあわせてアップデートしていかないといけない。
メーカーによっても型番によってもCPUが異なる
AMD vs Intel vs ARM
AMDはIntelでコンパイルしたやつに意地でも対応しようとするけどARMはそんなに頑張ろうとしてない
クロスコンパイラには時間がかかる
printf とか main とか、コンパイルしたときに残っている名前のこと
$ v6nm a.out 001116T _exit 001014T _flush 001276B _fout 000030T _main 000054T _printf 000736T _putchar 000320t charac 001146T cret 000000a crt0.o 001130T csv 001130a csv.o 000242t decimal 001116a exit.o 000724a ffltpr.o 001030t fl 000464t float 001266b formp 000640t gnum 000030a hello.o 000362t hex 000256t logical 000100t loop 001272b ndfnd 001274b ndigit 000370t octal 000724T pfloat 000532t prbuf 000054a printf.o 000542t prstr 000724T pscien 000736a putchr.o 000520t remote 001270b rjust 001262B savr5 000502t scien 000000t start 000336t string 001174d swtab 001264b width 000030t ~main
アドレス | 種類 | 名前 |
---|---|---|
001116 | T | _exit |
001014 | T | _flush |
001276 | B | _fout |
000030 | T | _main |
000054 | T | _printf |
000736 | T | _putchar |
000320 | t | charac |
001146 | T | cret |
000000 | a | crt0.o |
001130 | T | csv |
場所と名前がシンボル情報を表示している
ゆえにアドレスの表示が違う
>>> 030 24 >>> 0x18 24 >>> hex(030) '0x18' >>> hex(030) '0x18' >>> oct(0x18) '030'
(当時のCPUは8進数がデフォルト)
シンボルを削ればファイルサイズを縮小できる。というのも、シンボルそのものはファイルの実行に関係ないから。
(あったらあったで便利だけど。読みやすいし。)
シンボル情報 = 名前 シンボルを削ると、nmコマンドが使えなくなる。
☁$ l a.out -rwxrwxrwx 1 shige staff 1186 2 1 14:14 a.out ☁$ v6strip a.out ☁$ l a.out -rwxrwxrwx 1 shige staff 706 2 1 14:55 a.out $ v6nm a.out no name list
逆アセンブルを影響を及ぼす。 シンボル情報が消えて、どこがなんだったのか分からなくなる。
バイナリを読むときに、シンボルがあったほうが圧倒的に楽。
そして、シンボルなしに読めるようにする必要もない。
$ 7run -d a.out 0000: f009 setd 0002: 1180 mov sp, r0 0004: 1226 mov (r0), -(sp) 0006: 0bd0 tst (r0)+ 0008: 1036 0002 mov r0, 2(sp) 000c: 09f7 0008 jsr pc, 0018 0010: 100e mov r0, (sp) 0012: 09df 024e jsr pc, *$024e 0016: 8901 sys 1 ; exit 0018: 0977 023c jsr r5, 0258 001c: 15ce 0274 mov $274, (sp) 0020: 09df 002c jsr pc, *$002c 0024: 0a00 clr r0 0026: 0100 br 0028 0028: 0077 023a jmp 0266 002c: 0977 0228 jsr r5, 0258 0030: e5c6 007e sub $7e, sp
printfn "Hello" let a = 1 printfn "a = %d" a printfn "Hello"
7shi / ikebin / wiki / pdp11 / hello — Bitbucket
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はファイルに書き込む関数
ファイルディスクリプタ
OSはファイルを数字で管理してて、それを直接みている write(1, hello, 6)で、 6は出力する文字数のこと。
ゆえに、標準出力でhello\nという文字を6文字出力する
main() { return 0; /* exit(1); */ }
exitしないとプログラムが暴走するので必ず書くこと。終了は明示的に書く
sys = システムコール命令…普通のプログラムと違い、OSが既に提供している
ユーザ→カーネルの呼び出しをシステムコールという。 OSが提供している特殊な関数=sys ユーザ定義の関数はsysではなくjsrで呼び出す
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はレジスタ。
Intel
mov ax, 1 /ax = 1;
ふああああ 代入の方向が違う。
変数がデスティネーション 中身がソース
AT&T(PDPと同じ方向で代入する) gccが使っているのがこちらで、オープンソース系はおもにこちら
mov $1, %ax
cpuが同じなのに文法が違う
最初の引数はレジスタに入れて、あとの引数はシステムコールのあとに入れる
バイナリはコードとデータが分けられている(例外はあるけど)
コードの部分のことはテキストといい、データの部分のことはデータという。
hello: <hello\n> / hello = "hello\n"; /変数名 vs 変数の中身
ゆえに、変数のなかみのほうを変えると表示されるものも変わる
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は「引数です」って話で、中身をダイレクトには表示しない。
データ領域は逆アセンブルしない
データはデータであって処理ではないから。テキスト領域の数字を解析するのが逆アセンブルの目的であるから。
実際に逆アセンブルされた箇所は右側。慣れれば読める。
バイナリは数字です!!
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は表示方法が違うだけで中身は同じ
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
ファイルは
の3つで構成されている
メモリの中を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を選ばずに開発ができる)
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
フツウにつかうエンディアンはリトルエンディアン
なおかつ、リトルエンディアンは1バイトごとに並べられている
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
基本のスクリプトに、ちょっとず色々な命令を付け足していって、アセンブラのことを知っていく。 で、わからなかったら仕様書に頼る。