概要
以下の記事の要点だけを、
- Cコードとアセンブリ命令を1対1で対応させて書き直し、
- ポインタを使うメリットをちょっと補足した記事です。
アセンブリ言語のさわりを知ればC言語のポインタもわかるよね、という話 - Qiita
ちなみに以下のアセンブリはARMのCPUのものですが、x86-64でもやっていることは同じです。
-
fp
は「フレームポインタ」- フレームポインタはメモリ領域の中の決まった場所を指していて、例えばリターンアドレスが格納されている位置を指している。
-
r3
は「汎用レジスタ」
あと、読みやすさのためにアセンブリコードの#
は削除しました。
サンプルコード
void main() {
int a;
int b;
b = 1;
a = b;
a = 2;
}
void main() {
int *a;
int b;
b = 1;
a = &b;
*a = 2;
}
アセンブリを使った解説
ポインタなし
int a;
int b;
a
もb
もint
なので、4バイトずつ離れたアドレス(a
: -8番地、b
: -12番地)を割り付ける(あとでコンパイラが)
b = 1;
mov r3, 1
str r3, [fp, -8]
- 数字の1をレジスタ
r3
にコピーmov
します。 -
r3
にコピーした値(1)を、メモリの-8番地(b)に代入str
します。
-8番地 | -12番地 |
---|---|
1 |
a = b;
ldr r3, [fp, -8]
str r3, [fp, -12]
- さっき-8番地(b)に代入した値を、もう一回レジスタ
r3
に読みとります(r3
には値(1)が入る)。 - レジスタ
r3
にコピーした値(1)を、-12番地(a)に代入str
します。
-8番地 | -12番地 |
---|---|
1 | 1 |
a = 2;
-8番地 | -12番地 |
---|---|
2 | 1 |
ポインタあり
int *a;
int b;
-
a
はint
を指すポインタ(32ビットアドレッシングの処理系であれば「ポインタのサイズは4バイト」なので結果的に-4としてオフセット) -
b
はint
- なので、4バイトずつ離れたアドレス(
a
: -12番地、b
: -8番地)を割り付ける - (あとでコンパイラが)
b = 1;
mov r3, 1
str r3, [fp, -12]
-
値(1)をレジスタ
r3
にコピーmov
します。 -
r3
にコピーした**値(1)**を、メモリの-12番地(b)に代入str
します。
-8番地 | -12番地 |
---|---|
1 |
a = &b;
sub r3, fp, 12
str r3, [fp, -8]
b
のアドレス&b
(-12番地)をaに代入します。
- 基準アドレス(
fp
)から12を引いたsub
アドレス(-12番地)をレジスタr3
に代入します(r3
にはアドレスが入る)。 - さっきレジスタ
r3
にコピーしたアドレス(-12番地)を-8番地(*a)に代入します。
-8番地 | -12番地 |
---|---|
-12番地 | 1 |
*a = 2;
*a
は-12番地の値なので、b=2
になる
-8番地 | -12番地 |
---|---|
-12番地 | 2 |
結論
ポインタは以下のようなときに使える
文字列操作
このように間接的に参照できるので、ポインタを使って以下のように文字列の文字を書き換えることができます。
char str[] = "hello";
for (char* p = str; *p != '\0'; p++) { // 1番地ずつ見ていく
if (*p == 'l') { // 'l'を探して
*p = '*'; // '*'に書き換える
}
}
char型のアドレスのオフセットは1なので、文字列の先頭のアドレスから舐めるように1番地ずつ順番に見ていくことができる。
-8番地 | -9番地 | -10番地 | -11番地 | -12番地 | -13番地 | |
---|---|---|---|---|---|---|
変更前 | h | e | l | l | o | \0 |
変更前 | h | e | * | * | o | \0 |
その他
以下のような使い方もできます
インクリメント関数
ポインタを使わない場合
void increment(int a)
{
a += 1;
}
int b;
b = 1;
increment(b);
と呼び出しても、引数の値は関数呼び出しでコピーになるので、bは1のまま。
ポインタを使う場合
void increment(int* a)
{
*a += 1;
}
int b;
b = 1;
increment(&b);
で、func内から呼び出し元の変数を書き換えることができます。
引数を1文字ずつ取り出す
例えば$git commit
ならば、例えば以下のようにメモリに格納されます:
argv[0] - 第0引数
-17番地 | -18番地 | -19番地 | -20番地 | -21番地 |
---|---|---|---|---|
$ | g | i | t | \0 |
argv[1] - 第1引数
-57番地 | -58番地 | -59番地 | -60番地 | -61番地 | -62番地 | -63番地 |
---|---|---|---|---|---|---|
c | o | m | m | i | t | \0 |
char *p = argv[1];
argv[1]
は第1引数の文字列のアドレス(-57番地)です。
謝辞
Kouji Matsui (@kekyo2) さん、matsujirushi (@matsujirushi12) さん、アドバイスありがとうございました!