#概要
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 関数でブレークポイントを設定すれば,ポインタ変数のデバッグができます.
#関連記事
- athrill(アスリル) を使用して TOPPERS OS(ASP3) をデバッグ
- マルチコア対応仮想環境(athrill)を使用して TOPPERS/ATK2 を実行する
- メモリ保護対応版 athrill(アスリル) のご紹介
- 今さらなぜCPUエミュレータを自作しようとおもったのか?
- athrill(アスリル)機能マニュアル
- athrill(アスリル) を使用してベアメタル・プログラミング(1回目:main関数が動き出すまで)
- athrill(アスリル) を使用してベアメタル・プログラミング(2回目:割り込みがソフトウェアに通知されるまで)
- TOPPERS/ATK2カーネル向け実機レス環境(athrill2)