#navi_header|C言語系| 「エキスパートCプログラミング」を読んでいたら、ポインタ/配列として定義したオブジェクトを別のファイルで配列/ポインタとして宣言すると動作がおかしくなるよ、誤解してるよ、という部分があったので、ちょっと面白そうなので試してみた。 #more|| #outline|| ---- * 定義と宣言の型を一致させる「正しい」使い方をしてみる。 まず「正しい」使い方をしてみる。 aryptr0.c というファイルに、char型のポインタ(ptr1)と配列(ary1)を定義する。 char *ptr1 = "Hello"; char ary1[] = "World"; これを、型を変えずに宣言して別ファイルの中で使ってみる:aryptr1.c #code|c|> #include extern char *ptr1; extern char ary1[]; int main() { char c; c = ptr1[2]; c = ary1[2]; return 0; } ||< それぞれコンパイルし、リンクする。 cc -Wall -g -c -o aryptr0.o aryptr0.c cc -Wall -g -c -o aryptr1.o aryptr1.c cc -Wall -o aryptr aryptr1.o aryptr0.o ** メモリ配置の確認 ptr1, ary1がどこのセクションに配置されたか確認してみる。まずはnmコマンドでアドレスを確認してみる。 $ nm aryptr ... 08049a9c D ary1 ... 080488d4 T main 08049a98 D ptr1 0x8049a98 にptr1, 4バイト後ろの 0x8049a9c に ary1が配置されている。ちなみにmain関数は0x80488d4に配置される。 ここでセクションヘッダーをチェックし、実際のセクション名を突き止める。 #pre||> $ objdump -h aryptr aryptr: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .interp 00000017 080480f4 080480f4 000000f4 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA (途中省略) 9 .text 000002d4 08048624 08048624 00000624 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 10 .fini 00000081 08048900 08048900 00000900 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 11 .rodata 000000e6 080489a0 080489a0 000009a0 2**5 CONTENTS, ALLOC, LOAD, READONLY, DATA 12 .data 0000001c 08049a88 08049a88 00000a88 2**2 CONTENTS, ALLOC, LOAD, DATA (以下省略) ||< ".data"セクションが丁度、0x8049a88 から 0x1c サイズ分ある。つまり、ptr1(0x8049a98), ary1(0x8049a9c)が入ってると思われる。 ということで".data"セクションをダンプしてみる。 #pre||> $ objdump -s -j .data aryptr aryptr: file format elf32-i386 Contents of section .data: 8049a88 a0890408 00000000 00000000 00000000 ................ 8049a98 808a0408 576f726c 64000000 ....World... ||< うまい具合に ptr1(0x8049a98)が2行目の先頭に来ている。その4バイト後に、ary1の配列初期化データ"World"が来ている。 またptr1の内容を見てみると "808a0408", エンディアンを戻せば 0x8048a80 を示しており、このアドレス範囲を含んでいるのは先の"objdump -h"の結果から ".rodata" セクションであることが分かる。ということで、".rodata"をダンプしてみる。 #pre||> $ objdump -s -j .rodata aryptr aryptr: file format elf32-i386 Contents of section .rodata: 80489a0 00000000 00000000 00000000 00000000 ................ ... 8048a80 48656c6c 6f00 Hello. ||< ちょうどキリの良いアドレスと言うこともあり、"Hello"という、ptr1の指しているデータが 0x8048a80 に存在することが確認出来た。 ** main関数の逆アセンブル ここまでで ptr1, ary1 のメモリ配置イメージは確認出来たので、いよいよmain関数の中でどう処理されるのかdisassembleしてみる。 main関数は0x80488d4、つまり".text"セクションにあるので、"--start-address"オプションを活用して次のようにobjdumpコマンドで逆アセンブルする。"-d"で逆アセンブル、"-S"で対応するソースコード行を出力、"-j"でセクション指定。 #pre||> $ objdump -d -S -j .text --start-address=0x80488d4 aryptr aryptr: file format elf32-i386 Disassembly of section .text: 080488d4
: 80488d4: 55 push %ebp 80488d5: 89 e5 mov %esp,%ebp 80488d7: 83 ec 18 sub $0x18,%esp extern char ary1[]; int main() { char c; c = ptr1[2]; 80488da: a1 98 9a 04 08 mov 0x8049a98,%eax 80488df: 83 c0 02 add $0x2,%eax 80488e2: 8a 10 mov (%eax),%dl 80488e4: 88 55 ff mov %dl,0xffffffff(%ebp) c = ary1[2]; 80488e7: a0 9e 9a 04 08 mov 0x8049a9e,%al 80488ec: 88 45 ff mov %al,0xffffffff(%ebp) return 0; 80488ef: 31 c0 xor %eax,%eax 80488f1: eb 01 jmp 80488f4 } 80488f3: 90 nop 80488f4: c9 leave 80488f5: c3 ret 80488f6: 89 f6 mov %esi,%esi ||< ptr1, ary1の処理をそれぞれ見てみる。 c = ptr1[2]; char型ポインタとして宣言された"ptr1"に配列の添字をつけている。配列の添字はオフセットに計算される。実際にアセンブラを呼んでみると、まず以下のmov命令で"ptr1"(0x8049a98)の中身がEAXレジスタに転送される。 80488da: a1 98 9a 04 08 mov 0x8049a98,%eax 先ほどメモリ配置を確認したとおり、EAXの中身はptr1の指しているアドレスの値、0x8048a80 になる。 続いて"[2]"が計算される。char型なので1バイト単位、つまり2バイト分追加となるのが次のadd命令になる。 80488df: 83 c0 02 add $0x2,%eax そしてEAXの値をアドレスとして、そこの値をDLに転送し、ローカル変数c(=スタック上の領域)に転送している。 80488e2: 8a 10 mov (%eax),%dl 80488e4: 88 55 ff mov %dl,0xffffffff(%ebp) 続いてchar型の配列として宣言された"ary1"の処理を見てみる。 c = ary1[2]; これは非常に素直に、"ary1"のアドレス 0x8049a9c に2を足したアドレス 0x8049a9e の値をALに転送し、続けてローカル変数c(=スタック上の領域)に転送している。 80488e7: a0 9e 9a 04 08 mov 0x8049a9e,%al 80488ec: 88 45 ff mov %al,0xffffffff(%ebp) ** gdbによるステップ実行確認 では実際にgdb上で動かして確認してみる。 #pre||> $ gdb aryptr GNU gdb 5.0nb1 (...) This GDB was configured as "i386--netbsdelf"... (gdb) b main Breakpoint 1 at 0x80488da: file aryptr1.c, line 8. (gdb) run Starting program: /home/msakamoto/lang.c/aryptr Breakpoint 1, main () at aryptr1.c:8 8 c = ptr1[2]; ||< ブレークポイントで停まったので、プログラムカウンタを見てみる。 (gdb) p/x $pc $1 = 0x80488da これは先ほどobjdumpで逆アセンブルした以下のコードの直前、ということが分かる。 80488da: a1 98 9a 04 08 mov 0x8049a98,%eax 早速 "next" と行きたいところだが、それだとC言語のソース1行丸ごと実行してしまう。 アセンブラのインストラクション単位で実行していきたいので、"i"をつけて "nexti" を実行し、プログラムカウンタを確認する。 (gdb) nexti 0x80488df 8 c = ptr1[2]; (gdb) p/x $pc $2 = 0x80488df この時点でのEAXレジスタを確認してみる。 (gdb) p/x $eax $3 = 0x8048a80 予想通り 0x8048a80 になっている。続けて"nexti"を実行すると、 80488df: 83 c0 02 add $0x2,%eax この命令が実行されるので、EAXには2が足されるはずだ。 (gdb) nexti 0x80488e2 8 c = ptr1[2]; (gdb) p/x $eax $4 = 0x8048a82 予想通り、0x8048a80 + 2 で 0x8048a82 になっている。では、このアドレスの中身をDLレジスタに転送させてみる。以下のコードが実行される筈。 80488e2: 8a 10 mov (%eax),%dl ということで"nexti"。 (gdb) nexti 0x80488e4 8 c = ptr1[2]; (gdb) p/x $dx $5 = Value can't be converted to integer. # あちゃー、この名前(dx)は使えないか。 (gdb) p/x $edx $6 = 0x6c "Hello"の"[2]"なので3文字目、"l"のASCIIコードがEDXに格納されたことが確認できた。 最後、逆アセンブルした以下のコードが実行される。 80488e4: 88 55 ff mov %dl,0xffffffff(%ebp) ということで"next"。(ここまで来れば"i"はつけなくても良いでしょう。) (gdb) next 9 c = ary1[2]; (gdb) print c $7 = 108 'l' ローカル変数"c"に"l"が格納されたことを確認出来た。 続けて"ary1"の処理を確認する。 (gdb) p/x $pc $8 = 0x80488e7 プログラムカウンタを見てみると、ちょうど次の命令の実行直前になっている。 80488e7: a0 9e 9a 04 08 mov 0x8049a9e,%al "nexti"で見ていく。 (gdb) nexti 0x80488ec 9 c = ary1[2]; (gdb) p/x $eax $9 = 0x8048a72 EAXが "0x8048a72"になっているが、"0x8048a"は前の結果なので気にしなくて良い。最後の"0x72"は"World"の3文字目、"r"のASCIIコードになっている。 予想通りの動きになっている。最後に、以下の命令が実行される。 80488ec: 88 45 ff mov %al,0xffffffff(%ebp) "nexti"で実行後、ローカル変数"c"に"r"が格納されたことを確認出来た。 (gdb) nexti 10 return 0; (gdb) print c $10 = 114 'r' (gdb) continue Continuing. Program exited normally. (gdb) quit * 定義と宣言が一致しない場合 長かったがここからが本題である。今までは「配列で定義されたオブジェクトは配列で宣言」「ポインタで定義された(以下略)」として「正しい」使い方をした場合を見てきたが、これを逆にしてみるとどうなるか確認したい。 aryptr2.c: #code|c|> #include extern char ptr1[]; extern char *ary1; int main() { char c; c = ptr1[2]; c = ary1[2]; return 0; } ||< ptr1, ary1を定義している実体ファイルは前に使ったのと同じ、aryptr0.cを使う。 char *ptr1 = "Hello"; char ary1[] = "World"; これにより、aryptr2.cでは「ポインタで定義された ptr1 を配列として宣言」「配列として定義された ary1 をポインタとして宣言」したことになる。 コンパイル、リンクしてみる。 cc -Wall -g -c -o aryptr2.o aryptr2.c cc -Wall -g -o aryptr2 aryptr2.o aryptr0.o ** メモリ配置の確認とmain関数の逆アセンブル "nm"コマンドでシンボルを確認する。 $ nm aryptr2 ... 08049a9c D ary1 ... 080488d4 T main 08049a98 D ptr1 ary1, ptr1に関しては変化していない。main関数をダンプしてみる。 #pre||> $ objdump -d -S -j .text --start-address=0x80488d4 aryptr2 aryptr2: file format elf32-i386 Disassembly of section .text: 080488d4
: 80488d4: 55 push %ebp 80488d5: 89 e5 mov %esp,%ebp 80488d7: 83 ec 18 sub $0x18,%esp extern char *ary1; int main() { char c; c = ptr1[2]; 80488da: a0 9a 9a 04 08 mov 0x8049a9a,%al 80488df: 88 45 ff mov %al,0xffffffff(%ebp) c = ary1[2]; 80488e2: a1 9c 9a 04 08 mov 0x8049a9c,%eax 80488e7: 83 c0 02 add $0x2,%eax 80488ea: 8a 10 mov (%eax),%dl 80488ec: 88 55 ff mov %dl,0xffffffff(%ebp) return 0; 80488ef: 31 c0 xor %eax,%eax 80488f1: eb 01 jmp 80488f4 } 80488f3: 90 nop 80488f4: c9 leave 80488f5: c3 ret 80488f6: 89 f6 mov %esi,%esi ||< ptr1とary1の処理が丁度 aryptr1.c のアセンブラコードと逆さまになっていることが分かる。ptr1は配列として、ary1はポインタとして処理するコードになっている。 ** gdbによるステップ実行確認 gdbを使いデバッグしてみる。 #pre||> $ gdb aryptr2 GNU gdb 5.0nb1 ... This GDB was configured as "i386--netbsdelf"... (gdb) b main Breakpoint 1 at 0x80488da: file aryptr2.c, line 8. (gdb) run Starting program: /home/msakamoto/lang.c/aryptr2 Breakpoint 1, main () at aryptr2.c:8 8 c = ptr1[2]; ||< ブレークポイントで停止した時点のプログラムカウンタを確認する。 (gdb) p/x $pc $1 = 0x80488da 次に実行される命令は次のmov命令であることが分かる。 80488da: a0 9a 9a 04 08 mov 0x8049a9a,%al "nexti"で実行してみる。 (gdb) nexti 0x80488df 8 c = ptr1[2]; (gdb) p/x $eax $2 = 0x8049b04 EAXの値が"0x8049b04"になっている。念のため、0x8049a9aにあった値を確認してみる。 (gdb) x/b 0x8049a9a 0x8049a9a : 0x04 "0x04"がALに転送されていることが確認出来た。"0x8049b"の部分は以前の計算結果が残っていたものと思われる。 引き続き実行してみる。 (gdb) nexti 9 c = ary1[2]; (gdb) print c $5 = 4 '\004' 実行されたのは次の命令であり、ALの値(0x04)がローカル変数"c"に転送されたことを確認出来た。 80488df: 88 45 ff mov %al,0xffffffff(%ebp) ここまでは(本来の挙動としては間違っているものの)セグメンテーションフォルトなどは発生せずに実行できた。 引き続き"ary1"の処理に進めてみる。 (gdb) nexti 0x80488e7 9 c = ary1[2]; (gdb) p/x $pc $6 = 0x80488e7 (gdb) p/x $eax $7 = 0x6c726f57 これは以下の命令が完了した直後の状態である。 80488e2: a1 9c 9a 04 08 mov 0x8049a9c,%eax ary1(=0x8049a9c)が、配列として定義されているのに、使う側ではポインタとして宣言されてしまった為、ary1のアドレスの中身がまずEAXに転送されてしまっている。 0x8049a9c[0] : "W"(=0x57) [1] : "o"(=0x6f) [2] : "r"(=0x72) [3] : "l"(=0x6c) [4] : "d"(=0x64) これの、ポインタ型つまり4バイト分がEAXに転送されたので、エンディアンの影響で EAX = "l"(0x6c), "r"(0x72), "o"(0x6f), "W"(0x57) = 0x6c726f57 となった。 ステップ実行を続けてみる。 80488e7: 83 c0 02 add $0x2,%eax この処理へnextiでステップ実行する。 (gdb) nexti 0x80488ea 9 c = ary1[2]; (gdb) p/x $eax $8 = 0x6c726f59 EAXの値に2が加算された。そして次の処理へステップ実行してみる。 80488ea: 8a 10 mov (%eax),%dl ここでセグメンテーションフォルトが発生する。 (gdb) nexti Program received signal SIGSEGV, Segmentation fault. 0x80488ea in main () at aryptr2.c:9 9 c = ary1[2]; EAXの値、0x6c726f59のアドレスを参照してみると、不正なメモリ参照でエラーになる。 (gdb) x/b 0x6c726f59 0x6c726f59: Error accessing memory address 0x6c726f59: Invalid argument. このように、配列定義をポインタとして宣言、あるいはポインタ定義を配列として宣言して処理しようとすると、不正メモリ参照を初めとするバグが発生することが確認出来た。 またgcc/gdbが使える場合は、アセンブラのインストラクション単位でステップ実行することにより、そうしたバグの発生原因を効率的に突き止めることが可能であることが分かった。 #navi_footer|C言語系|