5
1

More than 5 years have passed since last update.

Cのポインタとアセンブリの対応関係、およびポインタを使うメリット

Last updated at Posted at 2018-11-25

概要

以下の記事の要点だけを、

  1. Cコードとアセンブリ命令を1対1で対応させて書き直し、
  2. ポインタを使うメリットをちょっと補足した記事です。

アセンブリ言語のさわりを知れば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;

abintなので、4バイトずつ離れたアドレス(a: -8番地、b: -12番地)を割り付ける(あとでコンパイラが)

    b = 1;
mov     r3, 1
str     r3, [fp, -8]
  1. 数字の1をレジスタr3にコピーmovします。
  2. r3にコピーした(1)を、メモリの-8番地(b)に代入strします。
-8番地 -12番地
1
    a = b;
ldr     r3, [fp, -8]
str     r3, [fp, -12]
  1. さっき-8番地(b)に代入した値を、もう一回レジスタr3に読みとります(r3には値(1)が入る)。
  2. レジスタr3にコピーした(1)を、-12番地(a)に代入strします。
-8番地 -12番地
1 1
    a = 2;
-8番地 -12番地
2 1

ポインタあり

    int *a;
    int b;
  1. aintを指すポインタ(32ビットアドレッシングの処理系であれば「ポインタのサイズは4バイト」なので結果的に-4としてオフセット)
  2. bint
  3. なので、4バイトずつ離れたアドレス(a: -12番地、b: -8番地)を割り付ける
  4. (あとでコンパイラが)
    b = 1;
mov     r3, 1
str     r3, [fp, -12]
  1. 1)をレジスタr3にコピーmovします。
  2. r3にコピーした値(1)を、メモリの-12番地(b)に代入strします。
-8番地 -12番地
1
    a = &b;
sub     r3, fp, 12
str     r3, [fp, -8]

bのアドレス&b(-12番地)をaに代入します。

  1. 基準アドレス(fp)から12を引いたsubアドレス(-12番地)をレジスタr3に代入します(r3にはアドレスが入る)。
  2. さっきレジスタ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番地)です。

低レイヤを知りたい人のための Cコンパイラ作成入門

謝辞

Kouji Matsui (@kekyo2) さん、matsujirushi (@matsujirushi12) さん、アドバイスありがとうございました!

5
1
4

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
5
1