# 詳説Cポインタ

# Info

  • 書名: 『詳説Cポインタ』
  • 著者: Richard Reese
  • 読んだ日付: 2021.10 ~ 2021.12

# Motivation

  • 学科のC言語の授業の参考書だったため。
  • ポインタについて知ってることと知らないことを整理するため。
  • メモリについて理解を深めるため。
  • 典型的な脆弱性と、ベストプラクティスを知るため。

# Contents

# 1. ポインタの復習

  • nullの概念
    • NULLマクロは定数0をvoidへのポインタでキャストしたもの
    #define NULL ((void *)0)
    
    • ポインタにnullを代入するとは、そのポインタが何も指していない状態にすること. NULLはポインタへの用途を想定されている。
    int *pi;
    pi = 0; // OK
    pi = NULL; // OK
    pi = 100; // syntax error
    
    • voidへのポインタ = 汎用ポインタ
      • 2つのvoidへのポインタは両者がnull pointerであるときのみ等しい

# 2. C言語の動的メモリ管理

  • malloc
    • ヒープからメモリを動的に割り当てる
    • castはすべき
    • 引数はバイト数
    • static変数を初期化するときにmallocを呼び出すことは出来ない
      • static int *pi = malloc(sizeof(int));はアウト
  • calloc
    • malloc + memsetで0埋めと同様
    • callocのほうが高速
  • realloc
    • 割り当てたメモリを大きくしたり小さくしたり
  • alloca
    • 関数呼出しのスタックフレームにメモリを確保する。Freeが要らない。
    • 可搬性が悪いのであんまり使わない。使わないほうが良さそう。
  • free
    • 割り当てたメモリを解放
  • dangling pointer
    • 有効な領域を指していないPointer
  • 動的メモリ割当技術(非標準)
    • garbage collection
      • Boehm-Weiser GC
        • mallocをGC_MALLOCに置き換えると自動で解放される
      • RAII
      • 例外処理(MicrosoftのC言語)

# 3. ポインタと関数

  • スタック
    • メモリの下側(アドレスの大きい方)
    • 関数への引数や局所変数が入ったスタックフレームというデータが積まれていく
    • stack frame
      • return address
      • local変数格納用の領域
      • 引数格納用の領域
      • stack pointer, base pointer(frame pointer)
    • スタック内の変数のアドレスの相対的な順番は実行に対して不変
      • debugに役立つ
  • 関数にpointerを渡す
    • 関数引数はPointerであろうが値渡し
      • pointerの指す先は同一のもの。それを通じて関数内から変更する。
      • 例えばクソデカ構造体を渡す時、Pointerで渡す方が効率的
    • 効率のために参照渡ししたいが、変更もしてほしくない
      • 定数へのポインタを渡せばOK
    • pointerを返す
      • 呼び出し側でメモリ管理を完結
      • 関数内でmallocして呼び出し側が解放
  • 関数ポインタ
    • void(*foo)()
    • 関数ポインタを関数に渡す
      • e.g., sortのcomparator
    • 関数ポインタを関数から返す
    • 関数ポインタの配列
      • 特定の操作を並べて順番に行う、など。
    • あり得る欠点
      • 分岐予測の恩恵を受けられない可能性

# 4. ポインタと配列

  • mallocで動的に配列を作成
  • reallocで配列サイズ変更
  • 配列を関数に渡す
    • 配列記法
    • ポインタ記法
  • 2次元配列の割当
    • 配列として宣言
    int matrix[row][col] = {{...}, {...}, ...};
    
    は連続的に配置される。
    • 最初に外側の配列を割当て、各行にmallocでそれぞれ個別のメモリを割り当てる。
    int **matrix = (int**)malloc(rows * sizeof(int*));
    for (int i = 0; i < rows; ++i) {
        matrix[i] = (int *)malloc(cols * sizeof(int));
    }
    
    これは連続になる保証がない。
    • 最初に外側の配列を割当て、全ての行分のメモリを一気に割り当てる。
    int **matrix = (int **)malloc(rows * sizeof(int *));
    matrix[0] = (int *)malloc(rows * cols * sizeof(int));
    for (int i = 1; i < rows; ++i) {
        matrix[i] = matrix[0] + i * cols;
    }
    
    • C99以降
    int (*matrix)[cols] = (int (*)[cols])malloc(rows*cols*sizeof(int));
    
  • ジャグ配列
    • 宣言方法: コンパウンドリテラル
    int (*(arr[])) = {
        (int[]) {0, 1, 2},
        (int[]) {3, 4, 6, 3},
        (int[]) {0, 1}
    };
    

# 5. ポインタと文字列

  • 文字列定数はリテラルプールに配置
  • 文字定数はint型
  • strcmp
  • strcpy
    • mallocでdst側を確保null文字分+1することを忘れずに
  • strcat
    • 連結結果を保持するためのbufferを必ず確保する。横着してもとの配列・ポインタを使い回さない
  • 関数に文字列を渡す
    • できるだけconstに
    • const char *string
    • bufferの先頭のアドレスを返す
  • argc, argv
  • 関数から文字列を返したい
    • 文字列定数を返す
      • リテラルプールに配置されている
    • 関数内で動的に確保して返す
      • 解放は呼び出し側の責任
  • 関数ポインタ
    • 文字列の比較は標準的でない
    • 自作のComparatorを作って関数ポインタとしてSortする関数に渡す、など。

# 6. 構造体とポインタ

  • メモリ割り当て

    • 例えばintは4の倍数のアドレスに配置されなければならない、など。
    • ポインタ演算には注意が必要
    • 配列の要素間に隙間が生じうる
  • 構造体のメンバを動的確保した場合の解放

    • 構造体自体を解放してしまうと、中のメンバへのアクセスが不可能になり、リーク
    • deallocate用の関数を書く
  • malloc/freeのオーバーヘッド

    • malloc/freeはオーバーヘッドを生じ、ボトルネックになることがある

    • 割り当てられなくなったメモリのリストを作っておき、

      • getするときは
        • リストに構造体があれば取り出して再利用
        • なければ確保してreturn
      • returnするときは
        • リストに空きがあれば保存してreturn
        • 空きがなければdeallocate, freeしてNULLを返す

      とすればよい。

  • 構造体とポインタで実装できる基本的なデータ構造として

    • Linked List
    • Queue
    • Stack
    • Tree

    がある(配列でも実現可能)

# 7. セキュリティの問題と不適切なポインタの使用

  • アドレス空間配置のランダム化(Address Space Layout Randomization: ASLR)
    • メモリ内におけるデータ(code, stack, heap, ...)をランダムに配置することで攻撃者が予測できないようにする。
      • e.g., return-to-libc attack: libcを書き換えて実行制御を特定の場所に飛ばす
  • Data Execution Prevention
    • 実行不可領域に実行を移されないようにする
  • ポインタ宣言
    • int *ptr1, ptr2ではptr2はint型になる.宣言は独立した行にするべき。
    • 型のエイリアスは#defineではなくtypedef
    • 初期化を忘れない
    • 初期化忘れはNULL初期化, assert, ツールの利用などで防ぐ
  • ポインタ使用時の問題
    • バッファオーバーフロー
      • index out of range
        • sizeof(buffer)/sizeof(int)がint[]型bufferの長さになる
        • ポインタの型は合わせる。castは慎重に。
      • pointer演算
      • 入力でのオーバーフロー
      • strcpy, strcatなどの文字列操作
    • ポインタ演算は配列にのみ使うべき。メモリ上の配置を安易に仮定してはならない。
    • 関数呼び出しの()を忘れると、関数ポインタのアドレスが評価されてしまう。
      • 呼び出し結果を変数に入れて検査するとか
      • シグネチャが異なる関数のアドレスを関数ポインタに入れると未定義動作
  • メモリ解放に関わる問題
    • 多重解放
      • 解放済みポインタにはNULLを入れておく?いずれにせよロジックとして多重解放が起こらないようにすべき。
    • 重要なデータをメモリ上に配置し、使用し、もう使わなくなったら、上書きしておく
  • 静的解析ツール
    • gccの-Wallオプション

# 8. 残りの話題

  • 特殊な目的のアドレスへのアクセス

    • VIDEO_BASEなど、デバイスとのやりとりに使うアドレスへのアクセスでは、整数をポインタにキャストして使うということがある。ただし、整数とポインタとのキャストは予期せぬアドレスへのアクセスにつながるので要注意
  • ポートへのアクセス

    • ポートへのアクセスにもポインタを使う。
    • プログラムの外部にメモリを書き換える要因が存在する場合は、volatileを付ける。そうでないと、コンパイラの最適化により、次のようなことが起こりうる。
      • コンパイラの最適化により、メモリのデータをレジスタやキャッシュに退避させている間に外部要因でメモリ上のそのデータが書き換わると、予期せぬ動作を引き起こす。
  • エンディアン判定

  • restrictキーワード

    • あるポインタが別のポインタと同じアドレスを指している時、エイリアスという。
    • int * restrict ptrのようにrestrictキーワードをつけると、別名が無いことをコンパイラに伝えられる。最適化のヒントになりうる。
  • thread

    • thread間でポインタを共有
      • mutexを適切に叩く
    • コールバック関数の関数ポインタを構造体にカプセル化してthreadに渡す。
  • オブジェクト指向

    • Cで無理やりオブジェクト指向をする
    • ヘッダファイルで宣言だけ行い、構造体の実装とそれに依存する操作は全てソースファイルに書く(不透明ポインタ)ことで、カプセル化。中身を隠しておくことで、ユーザがメモリリークをやらかすリスクを減らせる。
    • ポリモーフィズム
      • 継承はサポートされていないので、構造体と関数ポインタ使って無理やり関係をシミュレートする。

# 感想

ポインタについて理解がかなり深まった。特に関数ポインタは具体的用途も把握できた。 典型的なバグやミス、習慣などは一通りさらえたので、実際にCで何か書く際に思い起こせるようにしていきたい。

Last Updated: 12/26/2021, 4:18:20 PM