🌑

【.NET】EmbeddedResource, UTF8リテラルはどこにあるのか?PE Image に入門

に公開
2025/12/22

はじめに

C#/.NETをお使いの皆さん。Fileclassなど以外のデータの実態の場所を考えることは少ないでしょう。大体managedですからね。GCがなんとかしてくれている。
 しかし、dllに紐づいたデータはどこにあるのか。どのようにロードされているのかはよく知らない。そう思って調べてみました。

言ってしまえば、ecma335に仕様がありますが、今回は話の焦点をUTF8リテラルと、EmbeddedResourceに合わせます。
https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf
https://github.com/dotnet/runtime/tree/main/docs/design/specs
https://github.com/stakx/ecma-335
https://learn.microsoft.com/en-us/dotnet/standard/assembly/

PE Image

dllは PE(Portable Executable)というフォーマットになっています。
II.25 File format extensions to PE

そしてのファイルの領域をセクションごとに読み取り専用で実行可能なMemory mapped file(Image)としてOSがマークします。[1]

https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information

ここでRVA (Relative Virtual Address)がでてきます。
Moduleをロードしたとき、その領域内の相対アドレスはいつロードしても変わりません。
そこで実際に割り振られたModuleのアドレス+RVAで実行時のアドレスが取得できるわけですね。

ツール

dllのデータ周りは標準ではこれらから扱えます。

  • System.Reflection.Metadata
  • System.Reflection.Metadata.Ecma335
  • System.Reflection.PortableExecutable

しかし解析目的だと使いづらいので
コードからは
https://github.com/0xd4d/dnlib
GUIとしては
https://github.com/dnSpyEx/dnSpy

を使いました。

EmbeddedResource

https://blog.yucchiy.com/2020/11/csharp-embedded-resources.html

EmbeddedResourceはdllに任意のファイルを埋め込む仕組みです。
今回の流れからわかるようにRVAが割り振られています。
実際に読んでみて、さらにGetManifestResourceStreamと一致しているか確かめましょう。

example.txt
This is an example.
csproj
<!-- 前後省略-->
 <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);
            
        }
    }
}
output
EmbeddedResourceTest...example.txt
This is an example.
True

できましたね。

実際にGetManifestResourceStreamがどのような処理になっているか気になった人はこちらから見れます。
https://github.com/dotnet/dotnet/blob/c93d94a7e854e38eb560698df4b1b3313e4efc13/src/runtime/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeAssembly.cs#L290-L300

https://github.com/dotnet/dotnet/blob/c93d94a7e854e38eb560698df4b1b3313e4efc13/src/runtime/src/coreclr/vm/peassembly.cpp#L502

UTF8リテラル

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/utf8-string-literals

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>

https://sharplab.io/#v2:D4AQDABCCMB0AqBTAHgFwNwFgBQIBM0OMAnABQCiAdgMYD2AJgJaUDmsAqvAGIAcsA4olQBlVACdmLUgCIAEogA2C2gBoIAdVpiF9AITSArjwCUx9EA=

コンパイラ生成された長さの指定された構造対に定数としてバイト列を紐づけています。

これも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);
            }
        }
    }
}
output
<PrivateImplementationDetails>/__StaticArrayInitTypeSize=14
Hello, World!
True

UTF8文字列のポインタを得られましたね。

おわりに

他にもリフレクションで得るデータや通常のstringの文字列リテラルとかILとかいろいろありますが、今回は実際に通常のC#からポインタに触れるものにRVAを使うことでもアクセスできることを確かめました。

sectionやstreamの紹介もしたかったですが、遅刻もしてるし省略します...

脚注
  1. 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