【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

メモリ書き込み構文

対象環境を限定できる場合、バイト配列にリトルエンディアンで32bit値を書き込むときに、私は以下のように書いていました。

char buf[32];
*(int *)&buf[6] = 0x12345678;

非常に分かりにくいとご指摘を受けて色々と変形しましたが、どれもしっくり来ません。

ポインタ演算
*(int *)(buf + 6) = 0x12345678;
記述を分割
int *p = (int *)&buf[6];
*p = 0x12345678;

アドレスをポインタで表現しているため、キャストに惑わされてメモリ操作のイメージが湧きにくいのではないかと思いました。そこで敢えて即値だけを使い、理屈抜きで基本構文として覚えてもらおうという趣旨のスライドを作ってみました。


アセンブリ言語

まずアセンブリ言語でメモリに書き込むコードを提示します。

MOV  BYTE PTR[0x00000001], 0x12
MOV  WORD PTR[0x00000004], 0xFECA
MOV DWORD PTR[0x0000000A], 0xEFBEADDE

要点は「サイズを指定してメモリに書き込む」ということです。もしサイズを指定せずに書いた場合、何バイト書き換えるのか分かりません。

× MOV [0x00000001], 0x12

そのため1バイトを指定しているのがBYTE PTRです。gasでは接尾辞を付ける書き方もあります。

MOVB [0x00000001], 0x12

即値指定

次にアセンブリ言語C言語に直訳して、即値でアドレスを指定して書き込むコードを提示します。

*(char  *)0x00000001 = 0x12;
*(short *)0x00000004 = 0xFECA;
*(long  *)0x0000000A = 0xEFBEADDE;

このようなコードはあまり見かけないため、説明用の擬似コードという印象を与えて、逆に混乱を招いたようです。でもこれは実際に使える構文で擬似コードではありません。

C言語で先頭の*はデリファレンスです。それが代入式であれば「メモリに書き込んでいる」と見なします。

使用例

アドレスの即値指定を実際に使ってみます。

【注】Windows Vista以降ではASLR(wikipedia:アドレス空間配置のランダム化)が推奨されていますが、今回は説明の便宜上、固定アドレスを使用します。

次のようなプログラムで配列のアドレスを調べます。

addr1.c
#include <stdio.h>

unsigned char buf[16];

int main(void) {
    int i;
    printf("%p:", buf);
    for (i = 0; i < sizeof(buf); i++)
        printf(" %02x", buf[i]);
    printf("\n");
    return 0;
}
実行結果
$ gcc addr1.c
$ ./a.exe
00405068: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

アドレスを即値指定してメモリを書き換えてみます。

addr2.c
#include <stdio.h>

unsigned char buf[16];

int main(void) {
    int i;
    *(char  *)0x00405069 = 0x12;
    *(short *)0x0040506c = 0xfeca;
    *(long  *)0x00405072 = 0xefbeadde;
    printf("%p:", buf);
    for (i = 0; i < sizeof(buf); i++)
        printf(" %02x", buf[i]);
    printf("\n");
    return 0;
}
実行結果
$ gcc addr2.c
$ ./a.exe
00405068: 00 12 00 00 ca fe 00 00 00 00 de ad be ef 00 00

実はこれ、たまたま配列のアドレスがずれなかったため即値指定できているに過ぎません。アドレスの保証は一切ないため、コード量の変化やグローバル変数の追加で簡単にずれます。そのため普通はこのような書き方はしません。

アセンブリ言語との対応を考えるための作為的なコードです。