LoginSignup
7
2

More than 5 years have passed since last update.

athrill(アスリル)を使用してC言語ポインタ変数を理解する

Last updated at Posted at 2018-03-24

概要

C 言語のポインタ変数はよく難しいと言われています.ポインタ変数を理解するには,RAM 上のメモリ配置と CPU レジスタの動きをイメージできるかどうかが大切だと思います.

そこで,今回は仮想マシン(athrill )を使用して,ポインタ変数の理解を深めてみたいと思います※.
※もうすぐ新入社員が入社する時期なので,新人さん向けの内容です.
※athrillは,教育目的にも使うことができます.

ポインタ変数とは

ポインタ変数は,通常の変数と同じように値を格納する変数です.
ポインタ変数が通常の変数と異なるのは,「メモリのアドレス」を専門的に扱う点です.

ポインタ変数の定義方法

たとえば,通常の int 型変数を定義する場合は,以下のようになります.

int global_value;

これに対して,int 型のポインタ変数を定義する場合は,以下のようになります.

int *global_value_pointer;

型のあとに 「*(アスタリスク)」をつけます.

ポインタ変数のアドレス配置

athrill で上記変数のアドレス配置を調べてみます.

まずは,「p コマンド」を使用して,通常変数(global_value)を調べてみましょう.

[DBG> p global_value
global_value = 0 (int:4) @ 0x5ff7408(0x0)

上記結果はこう読みます.

表記 意味
0 本変数に入っている値は0(10進表記)
(int:4) データ型はintでサイズは4バイト
@ 0x5ff7408 本変数のメモリ配置場所は0x5ff7408

※最後の(0x0)は,構造体の場合しか意味のない値ですので,無視してください.

次に,ポインタ変数(global_value_pointer)を調べてみましょう.

[DBG>p global_value_pointer
global_value_pointer = (int *: 4 ) 0x0  @ 0x5ff7000(0x0)
表記 意味
0x0 本変数に入っている値は0(16進表記)
(int *: 4 ) データ型はintのポインタ型でサイズは4バイト
@ 0x5ff7000 本変数のメモリ配置場所は0x5ff7000

上記結果を,メモリイメージ化するとこうなります.

アドレス アドレス割り当て変数
0x5ff7000 global_value_pointer 0x0
0x5ff7408 global_value 0

メモリイメージの視点で見ると,global_value と global_value_pointer には,
それぞれ同じようにアドレスが割り当てられており,値が格納されています.

ポインタ変数にアドレスを代入する

ここで,ポインタ変数にアドレスを代入してみましょう.

global_value_pointer = &global_value;

上記例は,global_value のアドレス(※)を global_value_pointerに代入しています.
(※) &を変数の前に付すと変数アドレスがとれます.

athrill で代入後の global_value_pointer の値を見てみましょう.

[DBG>p global_value_pointer
global_value_pointer = (int *: 4 ) 0x5ff7008  @ 0x5ff7000(0x0)

上記結果を,メモリイメージ化するとこうなります.

アドレス アドレス割り当て変数
0x5ff7000 global_value_pointer 0x5ff7008
0x5ff7008 global_value 0

global_value_pointerの領域に,global_value のアドレスの値が設定されていることがわかりますね.

ちなみに,新人さんがよくやるミスとして,こんなのがあります.

global_value_pointer = global_value;

この場合は,global_value のアドレスではなくて,値そのものを代入していますので,結果は以下のようになってしまいますのでご注意を.

p global_value_pointer
global_value_pointer = (int *: 4 ) 0x0  @ 0x5ff7000(0x0)

ポインタ変数で値代入する

それでは,ポインタ変数を使用して,値を代入してみましょう.

*global_value_pointer = 999;

ポインタ変数の前に「*(アスタリスク)」を付けると,ポインタ変数が格納しているアドレスの領域に値を設定します.
結果は,こうなります.

[DBG>p global_value
global_value = 999 (int:4) @ 0x5ff7408(0x0)
[DBG>p global_value_pointer
global_value_pointer = (int *: 4 ) 0x5ff7408  @ 0x5ff7000(0x0)

メモリイメージ化するとこうなります.

アドレス アドレス割り当て変数
0x5ff7000 global_value_pointer 0x5ff7408
0x5ff7008 global_value 999

global_value_pointer に格納されているアドレスが global_value ですので,
global_value のアドレス位置が結果として書き換わるわけです.

一連の流れをアセンブラ命令で読む

さて,ここまでくれば,ポインタ変数のメモリイメージがついてきたと思います.
次は,C言語レベルのポインタ変数操作が,CPU命令としてどのように扱われるのか理解を深めていきましょう.

以下の2行は,

    global_value_pointer = &global_value;
    *global_value_pointer = 999;

CPU命令(V850)になると,以下のようになります(最適化していません).

1行目:global_value_pointer = &global_value;

 87a:   2a 06 00 70     mov 0x5ff7000, r10 /* global_value_pointerのアドレスを r10 に代入(mov) */
 87e:   ff 05 
 880:   2b 06 08 74     mov 0x5ff7408, r11 /* global_valueのアドレスを r11 に代入(mov) */
 884:   ff 05 
 886:   6a 5f 01 00     st.w    r11, 0[r10] /* global_valueのアドレス(r11)を 
                                             * global_value_pointer が指すアドレス位置(r10)に格納(st.w)
                                             */

CPU の演算処理は,CPU レジスタを通して行われますので,メモリ上に格納されている値は一旦,CPU レジスタに格納する必要があるため,上記のような命令になります.

今回の場合は,CPUの汎用レジスタに変数のアドレスを設定しています.

汎用レジスタ 割り当て
r10 global_value_pointerのアドレス
r11 global_valueのアドレス

2行目:*global_value_pointer = 999;

 88a:   2a 06 00 70     mov 0x5ff7000, r10 /* global_value_pointerのアドレスを r10 に代入(mov) */
 88e:   ff 05 
 890:   2a 57 01 00     ld.w    0[r10], r10 /* global_value_pointer のアドレス(r10) が指すアドレス位置の値(0[r10])を
                                             * r10 にロード(ld.w)
                                             */
 894:   20 5e e7 03     movea   999, r0, r11 /* 即値(999) を r11 に代入(movea) */
 898:   6a 5f 01 00     st.w    r11, 0[r10] /* 999(r11) を 
                                             * global_value_pointer が指すアドレス位置(r10)に格納(st.w)
                                             */

以上のように,C 言語レベルのプログラムも,CPUが扱える命令に落とし込みすると,1行のプログラムも複数の命令に展開されることがわかります.

一連の流れを athrill でデバッグする

最初に,以下のプログラム実行直前で,ブレークを張り,CPUレジスタ状態を確認しましょう.

    global_value_pointer = &global_value;

上記プログラムのアドレス位置は 0x87a ですので,ここでブレークポイントを設定し,「cpu コマンド」でレジスタ状態を参照します(関係する箇所のみ抜粋).r10 と r11 はいずれも 0 が入っていることがわかります.

[DBG>b 0x87a
break 0x87a
[DBG>c
[CPU>
HIT break:0x87a main(+0x8)
[NEXT> pc=0x87a main.c 22
l
[DBG>cpu
PC              0x87a main(+0x8)
R0              0x0
R1              0x0
R2              0x0
R3              0x5ff7400 stack_data(+0x3fc) Stack Pointer
R4              0x0
R5              0x0
R6              0x0 Arg1
R7              0x0 Arg2
R8              0x0 Arg3
R9              0x0 Arg4
R10             0x0 Return Value
R11             0x0
R12             0x0
 :

次に,次の行の手前まで CPU のプログラムカウンタを進めます.

    *global_value_pointer = 999;

次の行のプログラムのアドレス位置は 0x88a ですので,ここでブレークポイントを設定し,「cpu コマンド」でレジスタ状態を参照します(関係する箇所のみ抜粋).r10 と r11 値が書き換わっていることがわかります.

[DBG>b 0x88a
break 0x88a
[DBG>c
[CPU>
HIT break:0x88a main(+0x18)
[NEXT> pc=0x88a main.c 24
cpu
PC              0x88a main(+0x18)
R0              0x0
R1              0x0
R2              0x0
R3              0x5ff7400 stack_data(+0x3fc) Stack Pointer
R4              0x0
R5              0x0
R6              0x0 Arg1
R7              0x0 Arg2
R8              0x0 Arg3
R9              0x0 Arg4
R10             0x5ff7000 global_value_pointer(+0x0) Return Value ★
R11             0x5ff7408 global_value(+0x0) ★
R12             0x0
 :

ここまでくると,global_value_pointerのアドレス領域(0x5ff7000)にglobal_valueのアドレスが格納されているはずですので,「print コマンド」で確かめてみましょう.

[DBG>p 0x5ff7000 4
size=4 byte
0x5ff7000 0x5ff7408

確かに入ってますね.

最後に,「 *global_value_pointer = 999;」の結果を確認しましょう.

[DBG>b 0x89c
break 0x89c
[DBG>c
[CPU>
HIT break:0x89c main(+0x2a)
[NEXT> pc=0x89c main.c 28
cpu
PC              0x89c main(+0x2a)
R0              0x0
R1              0x0
R2              0x0
R3              0x5ff7400 stack_data(+0x3fc) Stack Pointer
R4              0x0
R5              0x0
R6              0x0 Arg1
R7              0x0 Arg2
R8              0x0 Arg3
R9              0x0 Arg4
R10             0x5ff7408 global_value(+0x0) Return Value
R11             0x3e7 ★999
R12             0x0
 :

global_value_pointerが指すアドレス位置は 「0x5ff7408」であり,その領域に格納する値(999)は,
r11 に一時的に格納されています.

そして,global_value_pointerが指すアドレス領域にも 999(0x3e7)が入っていますね!

[DBG>p 0x5ff7408 4
size=4 byte
0x5ff7408 0x3e7 ★999

ポインタ演習用ソースコードの配置場所

ここでご説明したプログラム一式は athrill プロジェクトで公開しています.

場所は,以下になります.

https://github.com/tmori/athrill/tree/master/sample/barmetal/step1

main.c が,ポインタ演習用のプログラムです.
ビルドおよび athrill によるデバッグは,以下のコマンドを叩くことで簡易化しています.

tmori@tmori-vaio:~/project/athrill/sample/barmetal/step1$ ../build/run.sh
Elf loading was succeeded:0x0 - 0x980 : 2.384 KB
Elf loading was succeeded:0x980 - 0x980 : 0.0 KB
ELF SYMBOL SECTION LOADED:index=15
ELF SYMBOL SECTION LOADED:sym_num=41
ELF STRING TABLE SECTION LOADED:index=16
[DBG>
HIT break:0x0
[NEXT> pc=0x0 vector.S 6

上記メッセージ出力後,main 関数でブレークポイントを設定すれば,ポインタ変数のデバッグができます.

関連記事

7
2
0

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