8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?