Created
April 17, 2018 03:01
-
-
Save ufcpp/107ecf308acdcd78540bc8d07a52cc6b to your computer and use it in GitHub Desktop.
C# のポインターがらみ
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))); | |
} | |
} | |
} |
あ、単純に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
GcTracking()
について質問させてください。手元の環境(.NET Framework 4.8 x64 Debugビルド、.NET Core 3.1 x86 Releaseビルド)で追試をしたのですが、
fixed
が無くても同じ結果になってしまいます。(191行目と200行目の出力が同じ)
1度目のGCにより再配置されにくくなっている(gen2に移っている?)と考えていますが、2度目(196行目)で再配置を起こしやすくする方法が有ったりするのでしょうか。
GenerageGarbage()
のループを10_000_000
に増やしたり、GCモードのサーバー/ワークステーションを切り替えたりしても動作に変わりは有りませんでした。