Skip to content

Instantly share code, notes, and snippets.

@ufcpp
Created April 17, 2018 03:01
Show Gist options
  • Save ufcpp/107ecf308acdcd78540bc8d07a52cc6b to your computer and use it in GitHub Desktop.
Save ufcpp/107ecf308acdcd78540bc8d07a52cc6b to your computer and use it in GitHub Desktop.
C# のポインターがらみ
using System;
using System.Runtime.CompilerServices;
using static System.Console;
unsafe class Program
{
static void Main()
{
PointerToX();
PointerToArray();
PointerToString();
EmptyArray();
GcTracking();
}
/// <summary>
/// 参照型変数が指す先のヒープのアドレスを取得。
/// </summary>
/// <remarks>
/// <see cref="Unsafe"/> クラスは、C# では絶対に書けない処理をやってくれる(中身は IL assebler 実装)。
/// C# の unsafe コード以上に unsafe なことができるやべーやつ。
/// IL は案外がばがばで、C# コンパイラーのレベルで安全性を保証してることが結構ある。
/// </remarks>
static ulong AsPointer<T>(T r) where T : class => (ulong)Unsafe.As<T, IntPtr>(ref r);
/// <summary>
/// ref が指す先のアドレスを取得。
/// </summary>
static ulong AsPointer<T>(ref T r) => (ulong)Unsafe.AsPointer(ref r);
/// <summary>
/// C# の参照型が内部的にどうなっているか試してみるために、フィールド1個だけのクラスを用意。
/// </summary>
class X
{
// フィールドが1個だけなので、順序に悩む必要なし。
// クラスの場合、フィールドが複数あるとき、並び順はコンパイラーが自由に変えていい仕様になってるので注意。
// (StructLayout 属性を付けて制御はできる。)
public int Value;
}
/// <summary>
/// まずは、クラスと、そのフィールドへの参照について。
/// </summary>
private static void PointerToX()
{
WriteLine("--- class X ---");
var x = new X { Value = 12345678 };
// X の先頭。
WriteLine(AsPointer(x));
// Value (= X の最初のメンバー) の参照が指す先。
// 先頭から8バイトずれてる。
WriteLine(AsPointer(ref x.Value));
// この fixed で取れるのは「Value の参照が指す先」のアドレス。
fixed (int* p = &x.Value) WriteLine((ulong)p);
var ptr = AsPointer(x); // 先頭から
WriteLine(*(int*)((byte*)ptr + 8)); // 8バイト目に Value の値 = 12345678
// 先頭8バイトにはいわゆる「オブジェクト ヘッダー」が入ってる。型情報テーブルへのポインター。
// オブジェクト ヘッダーが何バイトかは未定義(変わる可能性あり)なので、ObjectToPointer は環境によっては意図しない動きになる。
// (fixed の方はどの環境でもちゃんとしたアドレスを返してくれる。)
}
/// <summary>
/// 配列の場合。
/// </summary>
private static void PointerToArray()
{
WriteLine("--- int[] ---");
var array = new int[] { 12345678, 22222222, 33333333, 44444444 };
// 配列の先頭。
WriteLine(AsPointer(array));
// 最初の要素の参照が指す先。
// 先頭から16バイトずれてる。
WriteLine(AsPointer(ref array[0]));
// この書き方をしても、取れるのは array[0] のアドレス(= 16バイトずれた位置)。
// C# コンパイラーが変換処理してそう。
// array.Length == 0 ? 0 : &array[0] 相当の IL コードが生成されてる。
fixed (int* p = array) WriteLine((ulong)p);
// この書き方でも同じく array[0] のアドレス。
fixed (int* p = &array[0]) WriteLine((ulong)p);
var ptr = AsPointer(array); // 先頭から
WriteLine(*(long*)((byte*)ptr + 8)); // 8バイト目に long で配列の長さ = 4
WriteLine(*(int*)((byte*)ptr + 16)); // 16バイト目に array[0] = 12345678
WriteLine(*(int*)((byte*)ptr + 20)); // 20バイト目に array[1] = 22222222
WriteLine(*(int*)((byte*)ptr + 24)); // 24バイト目に array[1] = 33333333
WriteLine(*(int*)((byte*)ptr + 28)); // 28バイト目に array[1] = 44444444
}
/// <summary>
/// string の場合。
/// </summary>
private static void PointerToString()
{
WriteLine("--- string ---");
var str = "abc";
// string の先頭。
WriteLine(AsPointer(str));
// この書き方で取れるのは先頭から12バイトずれた値。
// C# コンパイラーが System.Runtime.CompilerServices.RuntimeHelpers.OffsetToStringData を呼んで位置調整してるっぽい。
fixed (char* p = str) WriteLine((ulong)p);
var ptr = AsPointer(str); // 先頭から
WriteLine(*(int*)((byte*)ptr + 8)); // 8バイト目に int で文字列の長さ = 3
WriteLine(*(char*)((byte*)ptr + 12)); // 12バイト目に str[0] = `a`
WriteLine(*(char*)((byte*)ptr + 14)); // 14バイト目に str[0] = `b`
WriteLine(*(char*)((byte*)ptr + 16)); // 16バイト目に str[0] = `c`
}
/// <summary>
/// 空配列に対する fixed は null ポインターを返すという話。
/// </summary>
private static void EmptyArray()
{
WriteLine("--- 空配列 ---");
var array = new int[0];
fixed (int* p = array) WriteLine((ulong)p); // 0
try
{
// この書き方だと今度は例外になる。
fixed (int* p = &array[0]) WriteLine((ulong)p);
}
catch (IndexOutOfRangeException)
{
WriteLine("IndexOutOfRangeException");
}
// ちなみに、同じことを文字列でやると…
var str = "";
fixed (char* p = str)
{
WriteLine((ulong)p); // 0 じゃない…
WriteLine((int)p[0]); // "" は、実際には new char[1] { '\0' } と同じ状態になってる。文字列の後ろにヌル文字が1つ入ってる。
}
}
/// <summary>
/// managed ポインター(参照型の変数とか、ref 変数)は GC によってオブジェクトが移動したことを追える。
/// * ポインターは、fixed していない限り、オブジェクトの移動に追従しない。
/// なので、通常の手段では managed ポインターを fixed なしで * ポインター化できない。fixed 必須。
/// </summary>
private static void GcTracking()
{
WriteLine("--- GC Tracking ---");
// GC 誘発用に無駄オブジェクトを無駄に大量生成。
void GenerageGarbage()
{
for (int i = 0; i < 100000; i++)
{
var dummy = new object();
}
}
GenerageGarbage();
var x = new X { Value = 12345678 };
ref var r = ref x.Value;
// 通常ではない手段(Unsafe クラス)を使って、managed ポインターを無理やり数値化。
var addressOfX = AsPointer(x);
var addressOfValue = AsPointer(ref r);
WriteLine((addressOfX, addressOfValue));
GenerageGarbage();
// 強制 GC
GC.Collect(0, GCCollectionMode.Forced);
// 無理やり数値化した方のアドレスまでは追えないので、当然、前のアドレスのまま。
// もう無効なアドレスなので、ここに対して読み書きするとクラッシュ・セキュリティ ホールの原因になる。
WriteLine((addressOfX, addressOfValue));
// GC 発生後、アドレスが変わってる。
// 大体は前に移動しているはずなので、値が小さくなってる。
WriteLine((AsPointer(x), AsPointer(ref r)));
fixed (int* p = &x.Value)
{
// fixed している間はどれだけゴミを出そうが x は移動しない。
GenerageGarbage();
GC.Collect(0, GCCollectionMode.Forced);
// fixe 直前と変わってないはず。
WriteLine((AsPointer(x), AsPointer(ref r)));
}
}
}
@udaken
Copy link

udaken commented Oct 27, 2021

GcTracking()について質問させてください。

手元の環境(.NET Framework 4.8 x64 Debugビルド、.NET Core 3.1 x86 Releaseビルド)で追試をしたのですが、fixedが無くても同じ結果になってしまいます。
(191行目と200行目の出力が同じ)

--- GC Tracking ---
(2984375124808, 2984375124816)
(2984375124808, 2984375124816)
(2984373436288, 2984373436296)
(2984373436288, 2984373436296)

1度目のGCにより再配置されにくくなっている(gen2に移っている?)と考えていますが、2度目(196行目)で再配置を起こしやすくする方法が有ったりするのでしょうか。
GenerageGarbage()のループを10_000_000に増やしたり、GCモードのサーバー/ワークステーションを切り替えたりしても動作に変わりは有りませんでした。

@udaken
Copy link

udaken commented Oct 27, 2021

あ、単純に183行目の1度目のGCを消せばいいですね。
失礼しました。
下記の様に変更することで確認できました。

    private static void GcTracking()
    {
        WriteLine("--- GC Tracking ---");

        // GC 誘発用に無駄オブジェクトを無駄に大量生成。
        void GenerageGarbage()
        {
            for (int i = 0; i < 100000; i++)
            {
                var dummy = new object();
            }
        }

        GenerageGarbage();

        var x = new X { Value = 12345678 };
        ref var r = ref x.Value;

        // 通常ではない手段(Unsafe クラス)を使って、managed ポインターを無理やり数値化。
        var addressOfX = AsPointer(x);
        var addressOfValue = AsPointer(ref r);

        WriteLine((AsPointer(x), AsPointer(ref r)));

        fixed (int* p = &x.Value)
        {
            // fixed している間はどれだけゴミを出そうが x は移動しない。
            GenerageGarbage();
            GC.Collect(2, GCCollectionMode.Forced);

            // fixe 直前と変わってないはず。
            WriteLine((AsPointer(x), AsPointer(ref r)));
        }

        // 強制 GC
        GC.Collect(0, GCCollectionMode.Forced);

        // 無理やり数値化した方のアドレスまでは追えないので、当然、前のアドレスのまま。
        // もう無効なアドレスなので、ここに対して読み書きするとクラッシュ・セキュリティ ホールの原因になる。
        WriteLine((addressOfX, addressOfValue));

        // GC 発生後、アドレスが変わってる。
        // 大体は前に移動しているはずなので、値が小さくなってる。
        WriteLine((AsPointer(x), AsPointer(ref r)));
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment