【.NET】EmbeddedResource, UTF8リテラルはどこにあるのか?PE Image に入門
はじめに
C#/.NETをお使いの皆さん。Fileclassなど以外のデータの実態の場所を考えることは少ないでしょう。大体managedですからね。GCがなんとかしてくれている。
しかし、dllに紐づいたデータはどこにあるのか。どのようにロードされているのかはよく知らない。そう思って調べてみました。
言ってしまえば、ecma335に仕様がありますが、今回は話の焦点をUTF8リテラルと、EmbeddedResourceに合わせます。
PE Image
dllは PE(Portable Executable)というフォーマットになっています。
II.25 File format extensions to PE
そしてのファイルの領域をセクションごとに読み取り専用で実行可能なMemory mapped file(Image)としてOSがマークします。[1]
ここでRVA (Relative Virtual Address)がでてきます。
Moduleをロードしたとき、その領域内の相対アドレスはいつロードしても変わりません。
そこで実際に割り振られたModuleのアドレス+RVAで実行時のアドレスが取得できるわけですね。
ツール
dllのデータ周りは標準ではこれらから扱えます。
- System.Reflection.Metadata
- System.Reflection.Metadata.Ecma335
- System.Reflection.PortableExecutable
しかし解析目的だと使いづらいので
コードからは
GUIとしては
を使いました。
EmbeddedResource
EmbeddedResourceはdllに任意のファイルを埋め込む仕組みです。
今回の流れからわかるようにRVAが割り振られています。
実際に読んでみて、さらにGetManifestResourceStreamと一致しているか確かめましょう。
This is an example.
<!-- 前後省略-->
<ItemGroup>
<EmbeddedResource Include="./example.txt" />
</ItemGroup>
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using dnlib.DotNet;
using dnlib.IO;
var assembly = Assembly.GetExecutingAssembly();
var moduleAddress = Marshal.GetHINSTANCE(assembly.ManifestModule);
ModuleContext modCtx = ModuleDef.CreateModuleContext();
// moduleやmoduleAddressを直接渡せるが
// ファイルの情報のみでできることを確かめるためLocationからロード
using ModuleDefMD module = ModuleDefMD.Load(assembly.Location, modCtx);
var peImage = module.Metadata.PEImage;
foreach (var resource in module.Resources)
{
if (resource is EmbeddedResource embeddedResource)
{
Console.WriteLine(embeddedResource.Name);
var reader = embeddedResource.CreateReader();
unsafe
{
var resourceAddress = (byte*)(moduleAddress + (int)peImage.ToRVA((FileOffset)reader.StartOffset));
Console.WriteLine(Encoding.UTF8.GetString(
new ReadOnlySpan<byte>(resourceAddress, (int)reader.Length)));
var stream = (UnmanagedMemoryStream)assembly.GetManifestResourceStream(embeddedResource.Name)!;
Console.WriteLine(stream.PositionPointer==resourceAddress);
}
}
}
EmbeddedResourceTest...example.txt
This is an example.
True
できましたね。
実際にGetManifestResourceStreamがどのような処理になっているか気になった人はこちらから見れます。
UTF8リテラル
ReadOnlySpan<byte> str = "Hello, World!"u8;
いきなりSpanで受けていますが、そのデータはどこにあるでしょうか?
using System.Text;
using System;
Console.WriteLine(Encoding.UTF8.GetString("Hello, World!"u8));
上記を逆コンパイルしてみると、以下のようになります。
[CompilerGenerated]
internal class Program
{
private unsafe static void <Main>$(string[] args)
{
Console.WriteLine(Encoding.UTF8.GetString(new ReadOnlySpan<byte>(Unsafe.AsPointer(ref <PrivateImplementationDetails>.219942BB7CFA570C0C492BF7AF990A33C0C3DA45DCAA69B3C300CBD5AC03F40A), 13)));
}
}
[CompilerGenerated]
internal sealed class <PrivateImplementationDetails>
{
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 14)]
internal struct __StaticArrayInitTypeSize=14
{
}
internal static readonly __StaticArrayInitTypeSize=14 219942BB7CFA570C0C492BF7AF990A33C0C3DA45DCAA69B3C300CBD5AC03F40A/* Not supported: data(48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 00) */;
}
.class private auto ansi sealed '<PrivateImplementationDetails>'
extends [System.Runtime]System.Object
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)
// Nested Types
.class nested assembly explicit ansi sealed '__StaticArrayInitTypeSize=14'
extends [System.Runtime]System.ValueType
{
.pack 1
.size 14
} // end of class __StaticArrayInitTypeSize=14
// Fields
.field assembly static initonly valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=14' '219942BB7CFA570C0C492BF7AF990A33C0C3DA45DCAA69B3C300CBD5AC03F40A' at I_00002838
.data cil I_00002838 = bytearray (
48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 00
)
} // end of class <PrivateImplementationDetails>
コンパイラ生成された長さの指定された構造対に定数としてバイト列を紐づけています。
これもdnlibで見てみましょう。
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using dnlib.DotNet;
var assembly = Assembly.GetExecutingAssembly();
var moduleAddress = Marshal.GetHINSTANCE(assembly.ManifestModule);
ModuleContext modCtx = ModuleDef.CreateModuleContext();
using ModuleDefMD module = ModuleDefMD.Load(assembly.Location, modCtx);
foreach (var t in module.Types)
{
if (t.Name.StartsWith("<P"))
{
foreach (var f in t.Fields)
{
if (!f.HasFieldRVA) continue;
Console.WriteLine(f.FieldType.FullName);
unsafe
{
var strPointer = (byte*)moduleAddress + ((int)f.RVA);
Console.WriteLine(Encoding.UTF8.GetString(new ReadOnlySpan<byte>(strPointer, (int)f.GetFieldSize())));
Console.WriteLine(Unsafe.AsPointer(ref MemoryMarshal.GetReference("Hello, World!"u8))==strPointer);
}
}
}
}
<PrivateImplementationDetails>/__StaticArrayInitTypeSize=14
Hello, World!
True
UTF8文字列のポインタを得られましたね。
おわりに
他にもリフレクションで得るデータや通常のstringの文字列リテラルとかILとかいろいろありますが、今回は実際に通常のC#からポインタに触れるものにRVAを使うことでもアクセスできることを確かめました。
sectionやstreamの紹介もしたかったですが、遅刻もしてるし省略します...
-
Single file publish だとOnMemoryになるらしい https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/assembly-api-behavior-changes-for-single-file-publish ↩︎
Discussion