見出し画像

コンパイラが何をしているのか、ちょっとのぞいてみよう その1

古い話で申し訳ないけれど、私がソフトウェア開発の仕事を始めた頃は、C言語レベルでデバッグできる環境はありませんでした。

そのため、いつもアセンブラでデバッグです。
お陰様で(?)、コンパイラが何をしているのかはよくわかりました。

コンパイラがなにをしているのか、気になったことはありませんか?
ちょっと覗いてみましょう。

昨今はとにかくすごい。
スマホでC言語がコンパイルできて、スマホで gdb も使えてしまいます。
これさえあれば覗き放題です。


まずコードを用意しましょう。

2つの変数を足し算する関数です。

$ cat add.c
#include <stdio.h>
int add(int a, int b);

int main()
{
        int x;
        x = add(1, 2);
        printf("x = %d\n", x);
}


int add(int a, int b)
{
        return (a + b);
}

$

では、コンパイルしましょう。
シェルのスクリプトを使います。

$ cat add.sh
cc add.c -g -o add.o
$ sh add.sh
$ ll
total 51
-rw------- 1 u0_a472 u0_a472  152 Sep 11 20:52 add.c
-rwx------ 1 u0_a472 u0_a472 6936 Sep 13 12:55 add.o*
-rwx------ 1 u0_a472 u0_a472   21 Sep 13 12:55 add.sh*
$

出来上がりました。
では、gdbで動かしてみます。

$ gdb add.o
GNU gdb (GDB) 10.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

<中略>

Reading symbols from add.o...
(gdb)

まず、main関数まで実行します。

mainにブレークポイントを設定して。

(gdb) b main
Breakpoint 1 at 0x1700: file add.c, line 7.
(gdb)

実行します。

(gdb) run
Starting program: /data/data/com.termux/files/home/pointer/add.o
BFD: /system/bin/linker64: unknown type [0x13] section `.relr.dyn'

Breakpoint 1, main () at add.c:7
7               x = add(1, 2);
(gdb)

main関数で停止しました。
アセンブラを見てみましょう。

(gdb) a
Dump of assembler code for function main:
   0x00000055555566ec <+0>:     sub     sp, sp, #0x20
   0x00000055555566f0 <+4>:     stp     x29, x30, [sp, #16]
   0x00000055555566f4 <+8>:     add     x29, sp, #0x10
   0x00000055555566f8 <+12>:    mov     w0, #0x1                        // #1
   0x00000055555566fc <+16>:    mov     w1, #0x2                        // #2
=> 0x0000005555556700 <+20>:    bl      0x5555556728 <add>
   0x0000005555556704 <+24>:    stur    w0, [x29, #-4]
   0x0000005555556708 <+28>:    ldur    w1, [x29, #-4]
   0x000000555555670c <+32>:    nop
   0x0000005555556710 <+36>:    adr     x0, 0x5555555550
   0x0000005555556714 <+40>:    bl      0x55555567a0 <printf@plt>
   0x0000005555556718 <+44>:    mov     w0, wzr
   0x000000555555671c <+48>:    ldp     x29, x30, [sp, #16]
   0x0000005555556720 <+52>:    add     sp, sp, #0x20
   0x0000005555556724 <+56>:    ret
End of assembler dump.
(gdb) 

「=>」は今のプログラムカウンタの位置です。
ここを実行しようとしたところで止まっています。
これは main 関数のアセンブラです。

add 関数はどうでしょう。

(gdb) a add
Dump of assembler code for function add:
   0x0000005555556728 <+0>:     sub     sp, sp, #0x10
   0x000000555555672c <+4>:     str     w0, [sp, #12]
   0x0000005555556730 <+8>:     str     w1, [sp, #8]
   0x0000005555556734 <+12>:    ldr     w8, [sp, #12]
   0x0000005555556738 <+16>:    ldr     w9, [sp, #8]
   0x000000555555673c <+20>:    add     w0, w8, w9
   0x0000005555556740 <+24>:    add     sp, sp, #0x10
   0x0000005555556744 <+28>:    ret
End of assembler dump.
(gdb)

スマホでこんなことができるなんて。
ここまで来てアレなんですが、このアセンブラはいったいどの CPU のものなんでしょうか。

そもそも、スマホが、何の CPU を使っているのかも知らず(笑)。
調べてみると、スマホの CPU は ARM を使っていることが多いようです。
ARM のアセンブラは初めてなんですが、なんとなく見覚えのある感じだし、わからなかったらネット検索でなんとかなるのではないかという安易な考えで果敢に挑戦してみましょう。

しかしこの Linux で見えているのは、ネイティブコードなのか、はたまたエミュレーションコードなのか。
それもおいおい確認しておこう。

次にレジスタを見てみましょう。

(gdb) i r
x0             0x1                 1
x1             0x2                 2
x2             0x7ffffff198        549755810200
x3             0x7ffffff158        549755810136
x4             0x7ff6dd0080        549602525312
x5             0x7ff6d1f8c4        549601802436
x6             0x61642f617461642f  7017786214960948271
x7             0x742e6d6f632f6174  8371749082501177716
x8             0x0                 0
x9             0x2                 2
x10            0x1                 1
x11            0x0                 0
x12            0x0                 0
x13            0x6f2e6464612f7265  8011451169428697701
x14            0x73beb86e7520578   521269146530481528
x15            0x0                 0
x16            0x7ff377a130        549545550128
x17            0x7ff7f02ab8        549620558520
x18            0x7ff7e24000        549619646464
x19            0x7ffffff188        549755810184
x20            0x55555566ec        366503880428
x21            0x7ffffff198        549755810200
x22            0x1                 1
x23            0x0                 0
x24            0x0                 0
x25            0x0                 0
x26            0x0                 0
x27            0x0                 0
x28            0x0                 0
x29            0x7ffffff110        549755810064
x30            0x7ff36ff178        549545046392
sp             0x7ffffff100        0x7ffffff100
pc             0x5555556700        0x5555556700 <main+20>
cpsr           0x80001000          [ EL=0 SSBS N ]
fpsr           0x0                 0
fpcr           0x0                 0
(gdb)

もう、泣けてきます(うれしくて)。

アセンブラで1ステップ実行してみます。

(gdb) si
add (a=0, b=0) at add.c:13
13      {
(gdb)

そうすると次のようになります。
add 関数の中に入ったところです。

(gdb) a
Dump of assembler code for function add:
=> 0x0000005555556728 <+0>:     sub     sp, sp, #0x10
   0x000000555555672c <+4>:     str     w0, [sp, #12]
   0x0000005555556730 <+8>:     str     w1, [sp, #8]
   0x0000005555556734 <+12>:    ldr     w8, [sp, #12]
   0x0000005555556738 <+16>:    ldr     w9, [sp, #8]
   0x000000555555673c <+20>:    add     w0, w8, w9
   0x0000005555556740 <+24>:    add     sp, sp, #0x10
   0x0000005555556744 <+28>:    ret
End of assembler dump.
(gdb)

次に実行される命令はこれになります。

sub     sp, sp, #0x10

sub は引き算。
sp から 0x10 を引いて、結果を sp に設定します。
sp はスタックポインタ。
アセンブラにおいては重要なレジスタです。
アセンブラにおけるスタックの役割を話し出すと脱線が激しくなるのでいずれまたそのうちに。

今の sp の値を確認しておきましょう。

(gdb) i r sp
sp             0x7ffffff100        0x7ffffff100
(gdb)

1ステップ実行します。

(gdb) si
0x000000555555672c      13      {
(gdb)

もう一度 sp の内容を見てみましょう。

(gdb) i r sp
sp             0x7ffffff0f0        0x7ffffff0f0
(gdb)

確かに、 0x10 だけ引き算されています。


さて、その次は、この命令文になります。
似たようなコードが2行続いているのでまとめていきましょう。

str     w0, [sp, #12]
str     w1, [sp, #8]

翻訳するとこうなります。

レジスタ w0 と w1 を、スタックポインタのオフセット 12 と 8 にストアする。

絵にするとこんな感じです。

レジスタ w0 と w1 はどうなっているでしょう。
レジスタ x0 と x1 は64bit ですが、 w0 と w1 はそのうちの 32bit を指し示します。

ですから、x0 と x1 を見てみます。

(gdb) i r x0 x1
x0             0x1                 1
x1             0x2                 2
(gdb) 

一方、スタックの内容はどうでしょうか。

(gdb) x/2xw 0x7ffffff0f8
0x7ffffff0f8:   0x00000000      0x00000000
(gdb)

まだ、ゼロです。

2回ステップ実行します。

(gdb) si
0x0000005555556730      13      {
(gdb) si
14              return (a + b);
(gdb)

その後にもう一度スタックの内容を見てみましょう。

(gdb) x/2xw 0x7ffffff0f8
0x7ffffff0f8:   0x00000002      0x00000001
(gdb)

確かに変わりました。

続けましょう。


次も、よく似た2行です。

ldr     w8, [sp, #12]
ldr     w9, [sp, #8]

翻訳します。

スタックポインタのオフセット 12 と 8 の 32 ビットのデータをレジスタ w8 と w9 にロードする。

再び、絵を書いてみるとこんな風になります。

今の x8 と x9 はこうなっています。

(gdb) i r x8 x9
x8             0x0                 0
x9             0x2                 2
(gdb)

[sp, #8] と [sp, #12] はこの通り。

(gdb) x/2xw 0x7ffffff0f8
0x7ffffff0f8:   0x00000002      0x00000001
(gdb)

では、2ステップを実行します。

(gdb) si
0x0000005555556738      14              return (a + b);
(gdb) si
0x000000555555673c      14              return (a + b);
(gdb)

すると、レジスタはこうなります。

(gdb) i r x8 x9
x8             0x1                 1
x9             0x2                 2
(gdb)

x8 が 0x1 になりました。


次は、これ。

add     w0, w8, w9

翻訳。

w8 と w9 を足して、結果を w0 に設定する。

現在のレジスタはこちら。

(gdb) i r x0 x8 x9
x0             0x1                 1
x8             0x1                 1
x9             0x2                 2
(gdb)

では、やってみましょう。

(gdb) si
0x0000005555556740      14              return (a + b);
(gdb)

するとレジスタはこうなりました。

(gdb) i r x0 x8 x9
x0             0x3                 3
x8             0x1                 1
x9             0x2                 2
(gdb)

お見事。
加算成功です。

あとは、スタックポインタを元に戻して、リターンするだけです。

(gdb) a
Dump of assembler code for function add:
=> 0x0000005555556740 <+24>:    add     sp, sp, #0x10
   0x0000005555556744 <+28>:    ret
End of assembler dump.
(gdb)

結局、コンパイラは何をしたのでしょうか。

・・・・・。

アセンブラのことばかりに話を費やしてしまった。

長くなりすぎましたので、続きは次回に・・・。


この記事が気に入ったらサポートをしてみませんか?