by shigemk2

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

オブジェクトファイルって何

定義

オブジェクトファイル (object file) またはオブジェクトコード (object code) とは、コンパイラがソースコードを処理した結果生成される中間的なコード表現である。オブジェクトファイルの内容はバイナリであり、コンパクトで構文解析済みのコードである。

オブジェクトファイル - Wikipedia

内容

cのファイルをコンパイルしたら出てくるoファイルがそれ。

池袋バイナリ勉強会 コンパイルとアセンブラの話 #ikebin

資料

7shi / ikebin / wiki / pdp11 — Bitbucket

最終目標はOSとコンパイラを作る

40年前のUNIX v6 1975

  • 昔のOSであるため簡単であること
  • Cのコンパイラがセットになっている
  • 「比較的」シンプル

これ以上簡単なものになってしまうと実用性が皆無になる

自分でコンパイラを作って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

場所と名前がシンボル情報を表示している

  • nmコマンドは8進数
  • -dコマンドは16進数

ゆえにアドレスの表示が違う

(豆知識)Pythonで16進数 8進数変換

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

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

資料

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

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