home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/ポインタ・配列の定義と宣言メモ1

C言語系/ポインタ・配列の定義と宣言メモ1

C言語系 / ポインタ・配列の定義と宣言メモ1
id: 533 所有者: msakamoto-sf    作成日: 2010-01-03 18:51:35
カテゴリ: C言語 

「エキスパートCプログラミング」を読んでいたら、ポインタ/配列として定義したオブジェクトを別のファイルで配列/ポインタとして宣言すると動作がおかしくなるよ、誤解してるよ、という部分があったので、ちょっと面白そうなので試してみた。


定義と宣言の型を一致させる「正しい」使い方をしてみる。

まず「正しい」使い方をしてみる。

aryptr0.c というファイルに、char型のポインタ(ptr1)と配列(ary1)を定義する。

char *ptr1 = "Hello";
char ary1[] = "World";

これを、型を変えずに宣言して別ファイルの中で使ってみる:aryptr1.c

#include <stdio.h>
 
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に配置される。
ここでセクションヘッダーをチェックし、実際のセクション名を突き止める。

$ 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"セクションをダンプしてみる。

$ 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"をダンプしてみる。

$ 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"でセクション指定。

$ objdump -d -S -j .text --start-address=0x80488d4 aryptr

aryptr:     file format elf32-i386

Disassembly of section .text:

080488d4 <main>:
 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 <main+0x20>
}
 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上で動かして確認してみる。

$ 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:

#include <stdio.h>
 
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関数をダンプしてみる。

$ objdump -d -S -j .text --start-address=0x80488d4 aryptr2

aryptr2:     file format elf32-i386

Disassembly of section .text:

080488d4 <main>:
 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 <main+0x20>
}
 80488f3:       90                      nop
 80488f4:       c9                      leave
 80488f5:       c3                      ret
 80488f6:       89 f6                   mov    %esi,%esi

ptr1とary1の処理が丁度 aryptr1.c のアセンブラコードと逆さまになっていることが分かる。ptr1は配列として、ary1はポインタとして処理するコードになっている。

gdbによるステップ実行確認

gdbを使いデバッグしてみる。

$ 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 <ptr1+2>:     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が使える場合は、アセンブラのインストラクション単位でステップ実行することにより、そうしたバグの発生原因を効率的に突き止めることが可能であることが分かった。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-01-03 18:55:26
md5:817bfd9dbc699ddf8f9d48ed828f3512
sha1:a00f21c9780394ad55acc1d65e1bfe6585b702b5
コメント
コメントを投稿するにはログインして下さい。