LoginSignup
8
3

More than 1 year has passed since last update.

0と1で動いているところを実際に見てみよう

Last updated at Posted at 2022-02-09

この記事を書こうと思ったきっかけ

「コンピューターって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で以下のプログラムを入力し、カレントディレクトリに保存します。

asmsample.s
    .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を指定して開くとバイナリエディタで表示されます。
hexeditor.png

はい。何がなんだか全然わからないと思います。
これからこの中身を順を追って読んでいきます。

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

出ましたね。確かに先程書いたプログラムが格納されているようです。
逆アセンブルの結果は、左からアドレス、実際に格納されている値、値を逆アセンブルした結果の順に並んでいます。

ではバイナリエディタで表示している実際のファイルと比較してみましょう。

hexeditor2.png

先頭から0x54byte目を見てみると、先程逆アセンブルした結果と少し違うようです。よく見ると並びが逆転しています。
これはCPUがレジスタの値をメモリに書き込む際に先頭バイトから書き込むか、末尾バイトから書き込むかの仕様によるものです。
Armやx86のなどのCPUは「リトルエンディアン」と言い、末尾から書き込むためレジスタの幅(4byte)ずつ逆転してデータが格納されています。
そのため、読む時には4byteずつ末尾から読む必要があります。

litle.png

では、この"E3 A0 10 0F"がなぜ"MOV R1, #15"になるのでしょうか?

CPUの命令セット

CPUが外から与えられたデータを汎用的に処理するためには、「どのようなデータを受け取ったらどのように処理する」というきまりを定義して公開する必要があります。
この決まりのことをアーキテクチャと呼び、データの並びの定義のことを「命令セット」と呼びます。
先程の"E3 A0 10 0F"が"MOV R1, #15"と認識できるのも、この命令セットに従っているからです。

では、その命令セットを見てみましょう。
Armアーキテクチャの場合、以下のリンク先にドキュメントが公開されています。

以下が命令セットのサマリーです。
32bitのアーキテクチャなので、32bitの幅固定でビットのOn/Off(1 or 0)の組み合わせによって以下のいずれかの命令と判断されて処理されます。

Format Summary
ARM instruction set formats

命令はMOVでしたので、MOVの仕様と比較してみましょう。

上段が命令セットの定義とMOVの場合の指定方法、下段が実際に設定した値の16進数と2進数になります。
仕様を見ると、21bit~24bitの4bitが1101であればMOV命令を指定したことになります。
その場合、12bit~15bitには格納先のレジスタ番号、0bit~11bitには格納する値(即値)を指定します。
実際の値を見ると、確かにMOV命令を指定し、格納先レジスタに1を指定し、格納するデータには15を指定しています。

data.png

このCPUが直接理解することのできる命令を「機械語」と呼びます。
確かに0と1で動いていますね!

じゃあ直接機械語を触るとどうなるのか?

せっかくここまで来たので機械語を直接触ってみましょう。
先程のMOV命令でレジスタ1に格納していた値15(0x0F)を14(0x0E)にしてみましょう。
バイナリエディタで直接編集し保存します。

hexeditor3.png

早速実行してみましょう。

pi@raspberrypi:~ $ ./asmsample 
pi@raspberrypi:~ $ echo $?
19

実行ファイルを直接変更して処理内容を直接更新することができました!

いかがでしたでしょうか。
簡単なアセンブリ言語のプログラムを書いて実行ファイルを作成し、それがどのようにCPUに解釈されて動いているか、実際に0と1で動いている所まで眺めてきましたが、なんとなくイメージは掴めたでしょうか。

おまけ:ここまで来たらC言語で作った実行ファイルと比較してみたい

まったく同じ動きをする処理をC言語で書いたら、その実行ファイルはどうなっているでしょうか?実際にやってみましょう。

先程のアセンブリ言語の処理と同様、15+5の結果を返すだけのC言語のプログラムを作成します。

csample.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"と計算結果をセットするコードに置き換えられた一方、実行するために必要なその他の処理により複雑になっていることがわかりますね。

さいごに

普段、低レイヤーに触れることのない人向けにかなり簡略化して書いてみました。
知っている人から見ると色々拙い部分はあるかもしれませんがご勘弁ください。
個人的には昔低レイヤーを仕事で扱えたことでその後のプログラミングにかなり影響を受けた経験があるので、少しでも敷居を低く感じて興味を持ってくれる人がいると嬉しいなぁと思います。

8
3
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3