この記事を書こうと思ったきっかけ
「コンピューターって0と1で動いてるっていうけどいまいち実感が湧かない」っていう人、多くないですか?
いわゆる低レイヤーのプログラミングをしたことがないと、実感は湧かないと思います。
一方で実際の業務で低レイヤーのプログラミングを経験できるのは組み込み系など一部の領域に限られている。でもなんとなく理解しておきたいけど敷居高そう。。
この記事はそんな低レイヤーの世界になんとなく興味はあるけど学習の一歩を踏み出せない人向けに書きました。この記事が学び始めるきっかけになってくれると嬉しいです。(そのため、あえて細かい説明は省いています。)
この記事で実現すること
低レイヤーといえばアセンブリ言語、そして機械語。
まずはアセンブリ言語でごく簡単なプログラムを書き、それを実行形式にして動くことを確認したら、その実行ファイルがどのように機械語に変換されているのかを眺めていきます。
まず環境を用意する
まずは実際に触って学ぶための環境を準備します。
Linux系のほうが一通り道具も揃っているので入りやすいでしょう。
今回はラズパイ上で環境を準備します。
Raspberry Pi OS(32bit)をインストールする
Raspberry Pi Imagerを使用してRaspberry Pi OS(32bit)をインストールします。
この手順は本家のサイトを参照してください。
※本記事は32bit OSを前提に書いています。
Visual Studio Codeをインストールする
以下のコマンドでVisual Studio Code(VS Code)をインストールします。
pi@raspberrypi:~ $ sudo apt install -y code
インストール後、VS Codeを開き、以下の拡張機能もインストールしてください。
・Japanese Language Pack for Visual Studio (VS編集 Code日本語化パック)
・Hex Editor (バイナリエディタ)
・Arm Assembly (Armのアセンブリ言語のSyntax Support)
さっそくアセンブリ言語でプログラミングしてみる
VS Codeで以下のプログラムを入力し、カレントディレクトリに保存します。
.global _start
_start:
MOV R1, #15
MOV R2, #5
ADDS R0, R1, R2
MOV R7, #1
SWI 0
プログラムの内容について簡単に説明します。
MOV R1, #15
レジスタ1に値15をセットします。
MOV R2, #5
レジスタ2に値5をセットします。
ADDS R0, R1, R2
レジスタ1とレジスタ2の値を足してレジスタ0にセットします。
(レジスタ0にセットした値が終了ステータスとして返されます。)
MOV R7, #1
SWI 0
システムコール1(exit)をセットして処理を終了しコマンドプロンプトに制御を戻します。
次にターミナルを開き、作成したプログラムを実行形式にします。
pi@raspberrypi:~ $ as -o asmsample.o asmsample.s
pi@raspberrypi:~ $ ld -o asmsample asmsample.o
これで実行形式のasmsampleファイルができました。
早速実行してみましょう。
pi@raspberrypi:~ $ ./asmsample
pi@raspberrypi:~ $ echo $?
20
無事、15+5の処理結果が戻り値として返されていますね。
出来上がった実行ファイルを詳しく見てみる
では、asmsampleをVS Codeで開いて見てみましょう。
メニューの「ファイルを開く」からasmsampleを指定して開くとバイナリエディタで表示されます。
はい。何がなんだか全然わからないと思います。
これからこの中身を順を追って読んでいきます。
Linuxの実行形式のファイルはExecutable and Linkable Format(ELF)と呼ばれる形式になっています。
ELFのフォーマット詳細については割愛しますが、大まかにはELFファイル全体のヘッダー情報としてのELFヘッダー、及びプログラムヘッダー、セクションヘッダーの3種類のヘッダー情報と、これらののヘッダー情報から辿ることのできるデータから構成されています。
なお、Linuxではこの形式のファイルの中身を確認するためのコマンドとしてreadelfが用意されているので、コマンドを使って各ヘッダー情報を見ていきます。
まずはELFヘッダーの情報を見ていきます。
pi@raspberrypi:~ $ readelf -h asmsample
ELF ヘッダ:
マジック: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
クラス: ELF32
データ: 2 の補数、リトルエンディアン
Version: 1 (current)
OS/ABI: UNIX - System V
ABI バージョン: 0
型: EXEC (実行可能ファイル)
マシン: ARM
バージョン: 0x1
エントリポイントアドレス: 0x10054
プログラムヘッダ始点: 52 (バイト)
セクションヘッダ始点: 456 (バイト)
フラグ: 0x5000200, Version5 EABI, soft-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 40 (bytes)
Number of section headers: 6
Section header string table index: 5
これらの情報から、このファイルがELFの32bit形式であることやリトルエンディアンであること、プログラムヘッダーやセクションヘッダーの開始位置(ファイル先頭からのバイト数)などの情報がわかります。
次にプログラムヘッダーの一覧を見ていきます。
pi@raspberrypi:~ $ readelf -l asmsample
Elf ファイルタイプは EXEC (実行可能ファイル) です
エントリポイント 0x10054
There is 1 program header, starting at offset 52
プログラムヘッダ:
タイプ オフセット 仮想Addr 物理Addr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00010000 0x00010000 0x00068 0x00068 R E 0x10000
セグメントマッピングへのセクション:
セグメントセクション...
00 .text
複雑なプログラムを書けばここに多くのプログラムヘッダーが並びますが、今回は15+5のプログラムだけですので1つしかありません。
なお、実際のプログラムは.textセクションに格納されます。
では続いてセクションヘッダーを見ていきましょう。
pi@raspberrypi:~ $ readelf -S asmsample
There are 6 section headers, starting at offset 0x1c8:
セクションヘッダ:
[番] 名前 タイプ アドレス Off サイズ ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00010054 000054 000014 00 AX 0 0 4
[ 2] .ARM.attributes ARM_ATTRIBUTES 00000000 000068 000012 00 0 0 1
[ 3] .symtab SYMTAB 00000000 00007c 0000d0 10 4 5 4
[ 4] .strtab STRTAB 00000000 00014c 00004a 00 0 0 1
[ 5] .shstrtab STRTAB 00000000 000196 000031 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
y (purecode), p (processor specific)
.textセクションはオフセット0x000054、つまりこのasmsampleファイルの先頭から0x54byte目から格納されていると書かれています。
Linuxには実行形式から逆アセンブル(アセンブリ言語に逆起こし)するコマンドもあります。
このコマンドで先程書いたアセンブリ言語のプログラムが出てくるか見てみましょう。
pi@raspberrypi:~ $ objdump -d asmsample
asmsample: ファイル形式 elf32-littlearm
セクション .text の逆アセンブル:
00010054 <_start>:
10054: e3a0100f mov r1, #15
10058: e3a02005 mov r2, #5
1005c: e0910002 adds r0, r1, r2
10060: e3a07001 mov r7, #1
10064: ef000000 svc 0x00000000
出ましたね。確かに先程書いたプログラムが格納されているようです。
逆アセンブルの結果は、左からアドレス、実際に格納されている値、値を逆アセンブルした結果の順に並んでいます。
ではバイナリエディタで表示している実際のファイルと比較してみましょう。
先頭から0x54byte目を見てみると、先程逆アセンブルした結果と少し違うようです。よく見ると並びが逆転しています。
これはCPUがレジスタの値をメモリに書き込む際に先頭バイトから書き込むか、末尾バイトから書き込むかの仕様によるものです。
Armやx86のなどのCPUは「リトルエンディアン」と言い、末尾から書き込むためレジスタの幅(4byte)ずつ逆転してデータが格納されています。
そのため、読む時には4byteずつ末尾から読む必要があります。
では、この"E3 A0 10 0F"がなぜ"MOV R1, #15"になるのでしょうか?
CPUの命令セット
CPUが外から与えられたデータを汎用的に処理するためには、「どのようなデータを受け取ったらどのように処理する」というきまりを定義して公開する必要があります。
この決まりのことをアーキテクチャと呼び、データの並びの定義のことを「命令セット」と呼びます。
先程の"E3 A0 10 0F"が"MOV R1, #15"と認識できるのも、この命令セットに従っているからです。
では、その命令セットを見てみましょう。
Armアーキテクチャの場合、以下のリンク先にドキュメントが公開されています。
以下が命令セットのサマリーです。
32bitのアーキテクチャなので、32bitの幅固定でビットのOn/Off(1 or 0)の組み合わせによって以下のいずれかの命令と判断されて処理されます。
命令はMOVでしたので、MOVの仕様と比較してみましょう。
上段が命令セットの定義とMOVの場合の指定方法、下段が実際に設定した値の16進数と2進数になります。
仕様を見ると、21bit~24bitの4bitが1101であればMOV命令を指定したことになります。
その場合、12bit~15bitには格納先のレジスタ番号、0bit~11bitには格納する値(即値)を指定します。
実際の値を見ると、確かにMOV命令を指定し、格納先レジスタに1を指定し、格納するデータには15を指定しています。
このCPUが直接理解することのできる命令を「機械語」と呼びます。
確かに0と1で動いていますね!
じゃあ直接機械語を触るとどうなるのか?
せっかくここまで来たので機械語を直接触ってみましょう。
先程のMOV命令でレジスタ1に格納していた値15(0x0F)を14(0x0E)にしてみましょう。
バイナリエディタで直接編集し保存します。
早速実行してみましょう。
pi@raspberrypi:~ $ ./asmsample
pi@raspberrypi:~ $ echo $?
19
実行ファイルを直接変更して処理内容を直接更新することができました!
いかがでしたでしょうか。
簡単なアセンブリ言語のプログラムを書いて実行ファイルを作成し、それがどのようにCPUに解釈されて動いているか、実際に0と1で動いている所まで眺めてきましたが、なんとなくイメージは掴めたでしょうか。
おまけ:ここまで来たらC言語で作った実行ファイルと比較してみたい
まったく同じ動きをする処理をC言語で書いたら、その実行ファイルはどうなっているでしょうか?実際にやってみましょう。
先程のアセンブリ言語の処理と同様、15+5の結果を返すだけのC言語のプログラムを作成します。
int main()
{
return 15+5;
}
コンパイルして実行形式のファイルを作成します。
pi@raspberrypi:~ $ gcc csample.c -Os -o csample
動かしてみましょう。
同じ結果が得られましたね。
pi@raspberrypi:~ $ ./csample
pi@raspberrypi:~ $ echo $?
20
では、この実行形式のファイルをまた逆アセンブルするとどうなるでしょうか?
pi@raspberrypi:~ $ objdump -d csample
csample: ファイル形式 elf32-littlearm
セクション .init の逆アセンブル:
0001029c <_init>:
1029c: e92d4008 push {r3, lr}
102a0: eb00001f bl 10324 <call_weak_fn>
102a4: e8bd8008 pop {r3, pc}
セクション .plt の逆アセンブル:
000102a8 <.plt>:
102a8: e52de004 push {lr} ; (str lr, [sp, #-4]!)
102ac: e59fe004 ldr lr, [pc, #4] ; 102b8 <.plt+0x10>
102b0: e08fe00e add lr, pc, lr
102b4: e5bef008 ldr pc, [lr, #8]!
102b8: 00010d48 .word 0x00010d48
000102bc <__libc_start_main@plt>:
102bc: e28fc600 add ip, pc, #0, 12
102c0: e28cca10 add ip, ip, #16, 20 ; 0x10000
102c4: e5bcfd48 ldr pc, [ip, #3400]! ; 0xd48
000102c8 <__gmon_start__@plt>:
102c8: e28fc600 add ip, pc, #0, 12
102cc: e28cca10 add ip, ip, #16, 20 ; 0x10000
102d0: e5bcfd40 ldr pc, [ip, #3392]! ; 0xd40
000102d4 <abort@plt>:
102d4: e28fc600 add ip, pc, #0, 12
102d8: e28cca10 add ip, ip, #16, 20 ; 0x10000
102dc: e5bcfd38 ldr pc, [ip, #3384]! ; 0xd38
セクション .text の逆アセンブル:
000102e0 <main>:
102e0: e3a00014 mov r0, #20
102e4: e12fff1e bx lr
000102e8 <_start>:
102e8: e3a0b000 mov fp, #0
102ec: e3a0e000 mov lr, #0
102f0: e49d1004 pop {r1} ; (ldr r1, [sp], #4)
102f4: e1a0200d mov r2, sp
102f8: e52d2004 push {r2} ; (str r2, [sp, #-4]!)
102fc: e52d0004 push {r0} ; (str r0, [sp, #-4]!)
10300: e59fc010 ldr ip, [pc, #16] ; 10318 <_start+0x30>
10304: e52dc004 push {ip} ; (str ip, [sp, #-4]!)
10308: e59f000c ldr r0, [pc, #12] ; 1031c <_start+0x34>
1030c: e59f300c ldr r3, [pc, #12] ; 10320 <_start+0x38>
10310: ebffffe9 bl 102bc <__libc_start_main@plt>
10314: ebffffee bl 102d4 <abort@plt>
10318: 00010438 .word 0x00010438
1031c: 000102e0 .word 0x000102e0
10320: 000103d8 .word 0x000103d8
00010324 <call_weak_fn>:
10324: e59f3014 ldr r3, [pc, #20] ; 10340 <call_weak_fn+0x1c>
10328: e59f2014 ldr r2, [pc, #20] ; 10344 <call_weak_fn+0x20>
1032c: e08f3003 add r3, pc, r3
10330: e7932002 ldr r2, [r3, r2]
10334: e3520000 cmp r2, #0
10338: 012fff1e bxeq lr
1033c: eaffffe1 b 102c8 <__gmon_start__@plt>
10340: 00010ccc .word 0x00010ccc
10344: 00000018 .word 0x00000018
00010348 <deregister_tm_clones>:
10348: e59f0018 ldr r0, [pc, #24] ; 10368 <deregister_tm_clones+0x20>
1034c: e59f3018 ldr r3, [pc, #24] ; 1036c <deregister_tm_clones+0x24>
10350: e1530000 cmp r3, r0
10354: 012fff1e bxeq lr
10358: e59f3010 ldr r3, [pc, #16] ; 10370 <deregister_tm_clones+0x28>
1035c: e3530000 cmp r3, #0
10360: 012fff1e bxeq lr
10364: e12fff13 bx r3
10368: 00021024 .word 0x00021024
1036c: 00021024 .word 0x00021024
10370: 00000000 .word 0x00000000
00010374 <register_tm_clones>:
10374: e59f0024 ldr r0, [pc, #36] ; 103a0 <register_tm_clones+0x2c>
10378: e59f1024 ldr r1, [pc, #36] ; 103a4 <register_tm_clones+0x30>
1037c: e0413000 sub r3, r1, r0
10380: e1a01fa3 lsr r1, r3, #31
10384: e0811143 add r1, r1, r3, asr #2
10388: e1b010c1 asrs r1, r1, #1
1038c: 012fff1e bxeq lr
10390: e59f3010 ldr r3, [pc, #16] ; 103a8 <register_tm_clones+0x34>
10394: e3530000 cmp r3, #0
10398: 012fff1e bxeq lr
1039c: e12fff13 bx r3
103a0: 00021024 .word 0x00021024
103a4: 00021024 .word 0x00021024
103a8: 00000000 .word 0x00000000
000103ac <__do_global_dtors_aux>:
103ac: e92d4010 push {r4, lr}
103b0: e59f4018 ldr r4, [pc, #24] ; 103d0 <__do_global_dtors_aux+0x24>
103b4: e5d43000 ldrb r3, [r4]
103b8: e3530000 cmp r3, #0
103bc: 18bd8010 popne {r4, pc}
103c0: ebffffe0 bl 10348 <deregister_tm_clones>
103c4: e3a03001 mov r3, #1
103c8: e5c43000 strb r3, [r4]
103cc: e8bd8010 pop {r4, pc}
103d0: 00021024 .word 0x00021024
000103d4 <frame_dummy>:
103d4: eaffffe6 b 10374 <register_tm_clones>
000103d8 <__libc_csu_init>:
103d8: e92d47f0 push {r4, r5, r6, r7, r8, r9, sl, lr}
103dc: e1a07000 mov r7, r0
103e0: e59f6048 ldr r6, [pc, #72] ; 10430 <__libc_csu_init+0x58>
103e4: e59f5048 ldr r5, [pc, #72] ; 10434 <__libc_csu_init+0x5c>
103e8: e08f6006 add r6, pc, r6
103ec: e08f5005 add r5, pc, r5
103f0: e0466005 sub r6, r6, r5
103f4: e1a08001 mov r8, r1
103f8: e1a09002 mov r9, r2
103fc: ebffffa6 bl 1029c <_init>
10400: e1b06146 asrs r6, r6, #2
10404: 08bd87f0 popeq {r4, r5, r6, r7, r8, r9, sl, pc}
10408: e3a04000 mov r4, #0
1040c: e4953004 ldr r3, [r5], #4
10410: e1a02009 mov r2, r9
10414: e1a01008 mov r1, r8
10418: e1a00007 mov r0, r7
1041c: e2844001 add r4, r4, #1
10420: e12fff33 blx r3
10424: e1560004 cmp r6, r4
10428: 1afffff7 bne 1040c <__libc_csu_init+0x34>
1042c: e8bd87f0 pop {r4, r5, r6, r7, r8, r9, sl, pc}
10430: 00010b24 .word 0x00010b24
10434: 00010b1c .word 0x00010b1c
00010438 <__libc_csu_fini>:
10438: e12fff1e bx lr
セクション .fini の逆アセンブル:
0001043c <_fini>:
1043c: e92d4008 push {r3, lr}
10440: e8bd8008 pop {r3, pc}
main()に記述した処理自体はコンパイラによって"mov r0, #20"と計算結果をセットするコードに置き換えられた一方、実行するために必要なその他の処理により複雑になっていることがわかりますね。
さいごに
普段、低レイヤーに触れることのない人向けにかなり簡略化して書いてみました。
知っている人から見ると色々拙い部分はあるかもしれませんがご勘弁ください。
個人的には昔低レイヤーを仕事で扱えたことでその後のプログラミングにかなり影響を受けた経験があるので、少しでも敷居を低く感じて興味を持ってくれる人がいると嬉しいなぁと思います。