[C言語] 変数の格納アドレス検証 char

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
# LLDB # コマンド

LLDBを使って変数のアドレスを調べています。

ゆくゆくはアセンブリ言語で簡単なコードを書くつもりなので、C++ではなくC言語で書いていきます。

今回はchar、char*について調べました。ポインタ自身のアドレスがわからなかったのですが、memory readコマンドの結果を照らし合わせて突き止めることができました。

下図の青線で囲んだところがポインタのようです。メモリアドレスが書かれています。変数にほぼ隣接した下位側に位置しています。

#include <stdio.h>

int main() {
    int i = 1000;
    char c[6] = {'a', 'b', 'c', 'd', 'e'};
    char* c_ptr = 'xyz';

    printf("i: %d\n", i);
    printf("c: %s\n", &c);
    printf("c_ptr: %s\n", &c_ptr);

    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
i: 1000
c: abcde
c_ptr: zyx // なぜ逆さになるのかは不明

[C/C++] 変数の格納アドレス検証 string

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

変数の格納アドレスを検証していきます。

LLDBを使って確認しました。

#include <stdio.h>
#include <iostream>
#include <string>

using std::cout; using std::endl;
using std::to_string;

int main() {
    int i = 256;
    char c[6] = {'a', 'b', 'c', 'd', 'e'};
    std::string str("abcde");

    cout << "iのアドレス: " << &i << endl;
    cout << "iのサイズ: " << to_string(sizeof i) << endl;

    cout << "cのアドレス: " << &c << endl;
    cout << "cのサイズ: " << to_string(sizeof c) << endl;

    cout << "strのアドレス: " << &str << endl;
    cout << "strのサイズ: " << to_string(sizeof str) << endl;

    return 0;
}

[C/C++] バッファオーバーフロー対策 -fstack-protector-allの実用性を検証 その2

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

引き続き、バッファオーバーフロー対策であるclang++の-fstack-protector-allオプションについて実用性を検証しました。

前回得られたデータから、char変数を増やしていくとやがてint変数のメモリ領域を上書きしてしまうと予想しましたが、実際にそうなるのか検証しました。

変数i予想値の算出方法:
2進数 1101000 00000000 00000000 00000000
反転 0010111 11111111 11111111 11111111
1加算 0011000 00000000 00000000 00000000
10進数 -402653184

その結果、char変数の数に関係なく、int変数との間に複数の空アドレスが配置されることが判明しました。

さすがに文法さえ正しければコードは正常に動くようです。

scanf関数の検証は今度こそこれで終わりにします。

#include <cppstd.h>

int main() {
    int i;
    char c[2];
    char c2[2];
    char c3[2];
    char c4[2];
    char c5[2];
    char c6[2];

    for (i=0; i< 10; i++){
        printf("for文先頭直後 printf i: %d\n",i);
        // rewind(stdin); // ストリームバッファをクリア

        scanf("%s", &c);
        cout << "cout c: " << c << endl;
        printf("printf c: %s\n",&c);

        scanf("%s", &c2);
        cout << "cout c2: " << c2 << endl;
        printf("printf c2: %s\n",&c2);

        scanf("%s", &c3);
        cout << "cout c3: " << c3 << endl;
        printf("printf c3: %s\n",&c3);

        scanf("%s", &c4);
        cout << "cout c4: " << c4 << endl;
        printf("printf c4: %s\n",&c4);

        scanf("%s", &c5);
        cout << "cout c5: " << c5 << endl;
        printf("printf c5: %s\n",&c5);

        scanf("%s", &c6);
        cout << "cout c6: " << c6 << endl;
        printf("printf c6: %s\n",&c6);
        
        printf("printf i: %d\n",i);
       
        cout << "for文終端" << endl;
    }
    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
for文先頭直後 printf i: 0
a
cout c: a
printf c: a
b
cout c2: b
printf c2: b
c
cout c3: c
printf c3: c
d
cout c4: d
printf c4: d
e
cout c5: e
printf c5: e
f
cout c6: f
printf c6: f
printf i: 0
for文終端
for文先頭直後 printf i: 1

[C/C++] バッファオーバーフロー対策 -fstack-protector-allの実用性を検証

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

バッファオーバーフロー対策であるclang++の-fstack-protector-allオプションについて実用性を検証しました。

その結果、char変数を複数宣言した場合、その間には空白アドレスが配置されずメモリアドレスの干渉が起こることが判明しました。

配列の文字数を正しく宣言すればいいのかというとそうでもなくて、char変数の数を増やすと下位アドレス側にメモリアドレスが伸びていき、ついにはint変数のメモリ領域が上書きされる可能性があります。

この程度で破綻するようでは、実用性は低いと判断するほかないです。このオプションは使わずに、char配列の文字数を正確に宣言するのが最善かと思います。

そもそも文法的に正しいC言語を書けば問題ないのであって、下手に補完して我々プログラマを甘やかすと余計に状況が悪くなるといった感じがします。

#include <cppstd.h>

int main() {
    int i;
    char c[2];  // ここを変えて検証
    char c2[2];  // ここを変えて検証

    for (i=0;i<5;i++){
        printf("for文先頭直後 printf i: %d\n",i);
        // rewind(stdin); // ストリームバッファをクリア
        scanf("%s", &c);

        cout << "cout c: " << c << endl;
        printf("printf c: %s\n",&c);

        scanf("%s", &c2);

        cout << "cout c2: " << c2 << endl;
        printf("printf c2: %s\n",&c2);

        cout << "2回目cout c: " << c << endl;
        printf("2回目printf c: %s\n",&c);

        printf("printf i: %d\n",i);
    }
    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
for文先頭直後 printf i: 0
a
cout c: a
printf c: a
b
cout c2: b
printf c2: b
2回目cout c: a
2回目printf c: a
printf i: 0
for文先頭直後 printf i: 1
a
cout c: a
printf c: a
b
cout c2: b
printf c2: b
2回目cout c: a
2回目printf c: a
printf i: 1
for文先頭直後 printf i: 2

[C/C++] バッファオーバーフロー対策 -fstack-protector-all

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

前回までscanf関数におけるバッファオーバーフローについて検証してきました。

コンパイラの対策としてgcc, g++, clang++などにはバッファオーバーフロー検知のメカニズムが実装されていて、未然に処置を施す仕組みになっています。

この機能はコンパイル時に-fstack-protector-allオプションを付加すると有効になります。

LLDBでその挙動を確認したところ、オーバーフローが発生しそうなchar変数をより上位アドレスに配置し、本来なら隣接するはずの別変数との位置関係が逆転してさらにその間に10バイトの空白アドレスが配置されました。

気をつけないといけないのはプログラムにてprintfなどでこれらの変数のメモリアドレスを出力させるとこの機能が無効になってしまう点です。

#include <cppstd.h>

int main() {
    int i;
    char c;

    for (i=0;i<5;i++){
        printf("for文先頭直後 printf i: %d\n",i);
        // rewind(stdin); // ストリームバッファをクリア
        scanf("%s", &c);

        cout << "cout c: " << c << endl;
        printf("printf c: %s\n",&c);

        printf("printf i: %d\n",i);
       
    }
    return 0;
--------------------------------------------------
出力
--------------------------------------------------
for文先頭直後 printf i: 0
a
cout c: a
printf c: a
printf i: 0
for文先頭直後 printf i: 1
a
cout c: a
printf c: a
printf i: 1
for文先頭直後 printf i: 2
(lldb) print &i
(int *) $1 = 0x000000016fdff57c
(lldb) print &c
(char *) $2 = 0x000000016fdff587 "a"

[C/C++] Cプログラミングの落とし穴 4.4 実引数 P68 scanf文法正常 aを連続入力成功

『Cプログラミングの落とし穴』(A.コーニグ, 1990)
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

ようやく正常な動作の紹介にたどりつきました。

きりがないのでscanf編はこれくらいにしておきます。

#include <cppstd.h>

int main() {
    int i;
    char c[2];

    cout << "iのアドレス: " << &i << endl;
    cout << "iのサイズ: " << to_string(sizeof i) << endl;

    cout << "cのアドレス: " << &c << endl;
    cout << "cのサイズ: " << to_string(sizeof c) << endl;

    for (i=0;i<5;i++){
        printf("for文先頭直後 printf i: %d\n",i);
        // rewind(stdin); // ストリームバッファをクリア
        scanf("%s", &c);

        cout << "cout c: " << c << endl;
        printf("printf c: %s\n",&c);

        printf("printf i: %d\n",i);
       
    }
    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
iのアドレス: 0x16f01f698
iのサイズ: 4
cのアドレス: 0x16f01f696
cのサイズ: 2
for文先頭直後 printf i: 0
a
cout c: a
printf c: a
printf i: 0
for文先頭直後 printf i: 1
a
cout c: a
printf c: a
printf i: 1
for文先頭直後 printf i: 2

[C/C++] Cプログラミングの落とし穴 4.4 実引数 P68 scanf文法正常 aを連続入力

『Cプログラミングの落とし穴』(A.コーニグ, 1990)
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

これまで扱ってきた不完全なコードについてscanf関数の引数を%sに修正しました。

しかし挙動はおかしいままでした。実はchar cが確保するメモリ領域が足りていません。ヌル文字の分も確保する必要があります。

ストリームバッファに改行コードが残っているはずなのにループが暴走しないのは何故なのか分かりません。

#include <cppstd.h>

int main() {
    int i;
    char c;

    cout << "iのアドレス: " << &i << endl;
    cout << "iのサイズ: " << to_string(sizeof i) << endl;

    cout << "cのアドレス: " << &c << endl;
    cout << "cのサイズ: " << to_string(sizeof c) << endl;

    for (i=0;i<5;i++){
        printf("for文開始直後 printf i: %d\n",i);
        // rewind(stdin); // ストリームバッファをクリア
        scanf("%s",&c);

        cout << "cout c: " << c << endl;
        printf("printf c: %d\n",c);

        printf("printf i: %d\n",i);
       
    }
    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
iのアドレス: 0x16b0df698
iのサイズ: 4
cのアドレス: 
cのサイズ: 1
for文開始直後 printf i: 0
a
cout c: a
printf c: 97
printf i: 0
for文開始直後 printf i: 1
a
cout c: a
printf c: 97
printf i: 0
for文開始直後 printf i: 1

[C/C++] Cプログラミングの落とし穴 4.4 実引数 P68 scanf文法ミス abcを入力 ストリームバッファ

『Cプログラミングの落とし穴』(A.コーニグ, 1990)
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

不完全なプログラムのscanfにabcを入力してみました。

ストリームバッファに改行コードが残っているためにループが止まらなくなり、i=5に達して終了しました。

ストリームバッファのデータは行方不明です。おそらく%dと一致しないデータ型を入力したため、undefinedな挙動になったものと思われます。

#include <cppstd.h>

int main() {
    int i;
    char c;

    cout << "iのアドレス: " << &i << endl;
    cout << "iのサイズ: " << to_string(sizeof i) << endl;

    cout << "cのアドレス: " << &c << endl;
    cout << "cのサイズ: " << to_string(sizeof c) << endl;

    for (i=0;i<5;i++){
        printf("for文開始直後 printf i: %d\n",i);
        // rewind(stdin); // ストリームバッファをクリア
        scanf("%d",&c);
        // 正しくは
        // scanf("%s",&c);

        cout << "cout c: " << c << endl;
        printf("printf c: %d\n",c);

        printf("printf i: %d\n",i);
       
    }
    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
iのアドレス: 0x16eeeb698
iのサイズ: 4
cのアドレス: 
cのサイズ: 1
for文開始直後 printf i: 0
abc
cout c: 
printf c: 0
printf i: 0
for文開始直後 printf i: 1
cout c: 
printf c: 0
printf i: 1
for文開始直後 printf i: 2
cout c: 
printf c: 0
printf i: 2
for文開始直後 printf i: 3
cout c: 
printf c: 0
printf i: 3
for文開始直後 printf i: 4
cout c: 
printf c: 0
printf i: 4

[C/C++] Cプログラミングの落とし穴 4.4 実引数 P68 scanf文法ミス 100を連続入力 ストリームバッファ

『Cプログラミングの落とし穴』(A.コーニグ, 1990)
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

不具合のあるプログラムに100を連続入力した場合の挙動を図にまとめました。

int iは1に増えますが、結局ヌル文字で上書きされるため、無限ループのようになります。

#include <cppstd.h>

int main() {
    int i;
    char c;

    cout << "iのアドレス: " << &i << endl;
    cout << "iのサイズ: " << to_string(sizeof i) << endl;

    cout << "cのアドレス: " << &c << endl;
    cout << "cのサイズ: " << to_string(sizeof c) << endl;

    for (i=0;i<5;i++){
        printf("for文開始直後 printf i: %d\n",i);

        scanf("%d",&c);
        // 正しくは
        // scanf("%s",&c);

        cout << "cout c: " << c << endl;
        printf("printf c: %d\n",c);

        printf("printf i: %d\n",i);
       
    }
    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
iのアドレス: 0x16fddf698
iのサイズ: 4
cのアドレス: 
cのサイズ: 1
for文開始直後 printf i: 0
100
cout c: d
printf c: 100
printf i: 0
for文開始直後 printf i: 1
100
cout c: d
printf c: 100
printf i: 0
for文開始直後 printf i: 1

[C/C++] Cプログラミングの落とし穴 4.4 実引数 P68 scanf文法ミス 1000入力 ストリームバッファ

『Cプログラミングの落とし穴』(A.コーニグ, 1990)
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

前回、scanfの引数を%sにすべきところを%dにしてしまった場合のプログラムの挙動を図に表現しました。

一部不正確なところがあったので改良版を作成しました。ストリームバッファの存在を明示すると実際により近い説明ができます。

scanfではストリームバッファに改行コードが残ってしまい、後々悪さをすることがあります。

#include <cppstd.h>

int main() {
    int i;
    char c;

    cout << "iのアドレス: " << &i << endl;
    cout << "iのサイズ: " << to_string(sizeof i) << endl;

    cout << "cのアドレス: " << &c << endl;
    cout << "cのサイズ: " << to_string(sizeof c) << endl;

    for (i=0;i<5;i++){
        scanf("%d",&c);
        // 正しくは
        // scanf("%s",&c);

        cout << "cout c: " << c << endl;
        printf("printf c: %d\n",c);

        printf("printf i: %d\n",i);
       
    }
    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
iのアドレス: 0x16f56f698
iのサイズ: 4
cのアドレス: 
cのサイズ: 1
1000
cout c: ?
printf c: -24
printf i: 3