Open9

Rustのメモリあれこれ

maru99maru99

あまり理解しないで使ってたのでまとめる。
個人的に理解している内容をまとめてみる。

ELFについて

この先の話をするにあたって、前提知識としてELFについて軽く知っている必要がある。
ELFとはExecutable and Linkable Formatの略であり、ファイルフォーマットの一種である。

rustだとcargo buildしたときにtarget/debug配下に生成されているファイルなどを指している認識。

このファイルを実行することでユーザーはプログラムを実行することができる。
内部的には以下のような構造になっている。

+----------------------------+
| ELF Header                 | <- ELF ファイルの先頭。ファイル形式やエントリーポイントを記載
+----------------------------+
| Program Header Table       | <- 実行時にカーネルが読む情報。ロードアドレスやメモリマッピング
+----------------------------+
| Section Header Table       | <- コンパイル時にリンカやデバッガが読む。各セクションの情報
+----------------------------+
| .text                      | <- 実行可能コード
| .rodata                    | <- 読み取り専用データ(定数文字列など)
| .data                      | <- 初期化済みグローバル変数
| .bss                       | <- 初期化されていないグローバル変数
| .debug_*                   | <- DWARF デバッグ情報
| .symtab / .strtab          | <- シンボルテーブル
| .eh_frame                  | <- stack unwinding 用情報
| .comment / .note           | <- コンパイラ情報、ビルド情報
+----------------------------+

簡単に各ヘッダーの補足をする。

1. ELF Header

他2つのヘッダへのオフセットやarchの情報を持っている。

$ readelf -h ./target/debug/dap-setup
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x78d0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          3982280 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         12
  Size of section headers:           64 (bytes)
  Number of section headers:         40
  Section header string table index: 39

2. Section Header

複数のオブジェクトファイルを結合するときにリンカが使用する。
また、逆アセンブルやデバックを行う際にもこの情報を使用する。

実行可能バイナリを作るために使用される情報。
また、ELFに対する解析を行う差異にもこの情報を参照する。

3. Program Header

OSカーネルがELFをロードして実行するときに使用する。

実行可能バイナリの作り方

ELFが作成される流れについて理解するために、コンパイル言語がソースコードからどのように実行可能バイナリを作るか確認する。

1. コンパイル

ソースコードに対して、C/C++ならgcc/clang, Rustならrustcを用いてコンパイルを行う。
この時に、ソースコードはアセンブリ(.s)に変換される。

*通常コンパイルとアセンブルは一括で行われるので、.sがディスク上に残ることはない。

2. アセンブル

コンパイラが生成したアセンブリファイル(.s)をアセンブラが機械語バイト列に変換し、ELF形式へと変換する。
この段階での生成物を**Object File(.o)**と呼ぶ。
ELF形式にはなっていますがProgram Headerを持っていないため実行することはできない。

3. リンク

上記のコンパイル・アセンブルによって複数のオブジェクトファイルが生成される。

リンカは全てのオブジェクトファイルのセクションをまとめて配置し直す。
これにより、各ファイルに分かれていた.text.dataが結合され、1つの大きなセクションになる。

また、リンカは複数の.oを結合する際に以下を行いセグメントを作成する

  • どのセクションをまとめて1つのセグメントにするかを決定する
  • セグメントのファイルオフセット、仮想アドレス、アクセス権限などを決定する

このセグメントの情報をまとめたものがProgram Header Tableになっている。

これにより、最終的にリンカは1つの実行可能バイナリを生成する。

メモリ領域について理解する

実行可能バイナリを実行したときの動きを理解するために、各メモリ領域について簡単に確認する。

1. 静的領域

プログラム実行時に一度だけ確保され、終了まで解放されないメモリ領域。
Rust的にはstaticで宣言された変数文字列リテラルグローバル変数はライフタイムが切れることがないので、静的領域に配置され、メモリが解放されることがない。

2. Stack領域

プロセス起動時にあらかじめ確保しているメモリ領域。
Rust的には変数のライフタイムが切れたときにスタックポインタを返却する

syscall的にはOSがプロセス起動時に固定サイズをmmapする。

3. Heap領域

スタックのように自動管理されていないメモリ領域。
プロセス起動時にあらかじめ領域を確保しているわけではないが、必要に応じてOSから割り当てることができる。

memory allocator関連の話で、実際にはmmapなどで大きな領域を確保しておいて、その中をmallocで管理する。
これにより小さなメモリ領域の確保にはsyscallをする必要が無くなる。

実行可能バイナリを実行する

上のメモリ領域の内容を前提として、実行ファイルを動かした時のメモリ領域の動きについて考える。

1. execve() syscallの呼び出し

プロセスが新しいプログラムを実行するためにexecve(2)を呼ぶ。
これによって、OSカーネルは実行ファイルを読み込んで、プロセス用のアドレス空間を作り始める。

2. ELFヘッダとプログラムヘッダを読み込む

カーネルはELFの先頭にあるELFヘッダから、実行可能なファイルかを確認する。
(プログラムヘッダの情報がなければ実行できない)

その後プログラムヘッダの情報を読み込み、各セグメントをどのメモリにマッピングするべきかを確認する。

3. メモリマッピング

プログラムヘッダに従ってセグメント単位でメモリを割り当てる

  • コードセグメント(.text, .rodata)
    • R-Xでマッピング
    • 実際にはmmap(2)でファイルのページをそのまま仮想メモリに対応付けする?
  • データセグメント(.data, .bss)
    • RW-でマッピング
    • .bssはゼロ初期化領域なので、ファイル上に存在せずゼロクリアされたページが割り当てられる?
  • スタック
    • カーネルが領域をmmap
    • プロセスごとにOSが上限サイズ(rlimit)を決めて用意
  • ヒープ
    • mallocからmmapが呼ばれることでメモリ確保

4. 動的リンカ

*動的リンカはRustで使ったことないので説明を飛ばします...

5. スタックの初期化

カーネルがスタックに以下をセットする。

  • argc(引数の数)
  • argv(引数の文字列配列)
  • envp(環境変数配列)
  • auxv(補助ベクタ:ELF のロード情報や CPU 機能フラグなど)

// TODO: あまり分かってないので調べる

6. エントリポイントへジャンプ

ELF ヘッダの e_entry にあるアドレス(エントリポイント)に制御が移る
動的リンクなし: いきなり main へ行かず、まず C ランタイム初期化コード (_start) へジャンプ
動的リンクあり: まずリンカがライブラリを解決して _start を呼ぶ

// TODO: あまり分かってないので調べる

maru99maru99

(memo) Virtual Function Tableについて

arg: &dyn TraitObject のような、TraitObjectを実装したオブジェクトを引数として取る関数がある場合、
この引数argは data pointervirtual function table の16byteをもつfat pointerになる。

  • data pointer
    • TraitObjectを実装しているオブジェクトの先頭アドレスが入る
    • 8byte(先頭アドレス)
    • Object自体のアドレスなので、Stack領域上を指すことが多い
  • virtual function table
    • TraitObjectを実装したObjectを引数として受け取る場合、TraitObjectの持っている関数群に対するアドレスを受け取る必要がある。
    • TraitObjectは複数の関数interfaceを持っている可能性があるが、関数なのでこのデータは静的領域上に配置されることになる。
    • このTraitObjectが持っている関数interfaceを実装した実態(静的領域上にある)をTraitごとにまとめたものを Vritula Function Table と呼んでいる
    • Virtual Functin Table は静的領域上に配置されている。
    • 引数としてはこのアドレス(8byte)を受け取ることになる。

備考として、

  • 関数は静的領域上に配置され、その関数を実行する度に引数となっている変数がStack領域(もしくはHeap領域)上でメモリの確保を行う

wip

maru99maru99

ELFにおけるSection Header Tableの詳細

Section Header Tableについて、個人的な認識を再度まとめると

  • ソースコードのコンパイルをしたときのアセンブリファイル(.s)やオブジェクトファイル(.o), リンクした後の実行バイナリについている。
  • 各セクションがファイルの中のどのオフセット、アドレスに配置されているかが書かれている。

Section Header Tableの構成

$ readelf -S ./target/debug/dap-setup
There are 40 section headers, starting at offset 0x3cc3c8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         00000000000002e0  000002e0
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.build-i NOTE             00000000000002fc  000002fc
       0000000000000024  0000000000000000   A       0     0     4
  [ 3] .note.ABI-tag     NOTE             0000000000000320  00000320
       0000000000000020  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000000340  00000340
       0000000000000024  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           0000000000000368  00000368
       0000000000000630  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000000998  00000998
       000000000000040d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000000da6  00000da6
       0000000000000084  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000000e30  00000e30
       0000000000000100  0000000000000000   A       6     4     8
  [ 9] .rela.dyn         RELA             0000000000000f30  00000f30
       0000000000004938  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000005868  00005868
       0000000000000060  0000000000000018  AI       5    27     8
  [11] .init             PROGBITS         0000000000006000  00006000
       000000000000001b  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000006020  00006020
       0000000000000050  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         0000000000006070  00006070
       0000000000000008  0000000000000008  AX       0     0     8
  [14] .text             PROGBITS         0000000000006080  00006080
       0000000000040973  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         00000000000469f4  000469f4
       000000000000000d  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000047000  00047000
       0000000000005517  0000000000000000   A       0     0     16
  [17] .debug_gdb_script PROGBITS         000000000004c517  0004c517
       0000000000000022  0000000000000001 AMS       0     0     1
  [18] .eh_frame_hdr     PROGBITS         000000000004c53c  0004c53c
       000000000000105c  0000000000000000   A       0     0     4
  [19] .eh_frame         PROGBITS         000000000004d598  0004d598
       0000000000005a50  0000000000000000   A       0     0     8
  [20] .gcc_except_table PROGBITS         0000000000052fe8  00052fe8
       0000000000001134  0000000000000000   A       0     0     4
  [21] .tdata            PROGBITS         0000000000055e08  00054e08
       0000000000000020  0000000000000000 WAT       0     0     8
  [22] .tbss             NOBITS           0000000000055e28  00054e28
       0000000000000040  0000000000000000 WAT       0     0     8
  [23] .init_array       INIT_ARRAY       0000000000055e28  00054e28
       0000000000000010  0000000000000008  WA       0     0     8
  [24] .fini_array       FINI_ARRAY       0000000000055e38  00054e38
       0000000000000008  0000000000000008  WA       0     0     8
  [25] .data.rel.ro      PROGBITS         0000000000055e40  00054e40
       0000000000002738  0000000000000000  WA       0     0     8
  [26] .dynamic          DYNAMIC          0000000000058578  00057578
       0000000000000230  0000000000000010  WA       6     0     8
  [27] .got              PROGBITS         00000000000587a8  000577a8
       0000000000000858  0000000000000008  WA       0     0     8
  [28] .data             PROGBITS         0000000000059000  00058000
       0000000000000068  0000000000000000  WA       0     0     8
  [29] .bss              NOBITS           0000000000059068  00058068
       0000000000000100  0000000000000000  WA       0     0     8
  [30] .comment          PROGBITS         0000000000000000  00058068
       0000000000000057  0000000000000001  MS       0     0     1
  [31] .debug_aranges    PROGBITS         0000000000000000  000580bf
       00000000000072d0  0000000000000000           0     0     1
  [32] .debug_info       PROGBITS         0000000000000000  0005f38f
       00000000000fd35d  0000000000000000           0     0     1
  [33] .debug_abbrev     PROGBITS         0000000000000000  0015c6ec
       00000000000011ed  0000000000000000           0     0     1
  [34] .debug_line       PROGBITS         0000000000000000  0015d8d9
       000000000006640e  0000000000000000           0     0     1
  [35] .debug_str        PROGBITS         0000000000000000  001c3ce7
       0000000000149898  0000000000000001  MS       0     0     1
  [36] .debug_ranges     PROGBITS         0000000000000000  0030d57f
       00000000000ac9d0  0000000000000000           0     0     1
  [37] .symtab           SYMTAB           0000000000000000  003b9f50
       0000000000005070  0000000000000018          38   524     8
  [38] .strtab           STRTAB           0000000000000000  003befc0
       000000000000d27c  0000000000000000           0     0     1
  [39] .shstrtab         STRTAB           0000000000000000  003cc23c
       000000000000018b  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

セクションヘッダはプログラムの実行にはあまり関係してこないので、一旦説明を飛ばす。

maru99maru99

ELFにおけるProgram Header Tableの詳細

$ readelf -l ./target/debug/dap-setup

Elf file type is DYN (Shared object file)
Entry point 0x78d0
There are 12 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002a0 0x00000000000002a0  R      0x8
  INTERP         0x00000000000002e0 0x00000000000002e0 0x00000000000002e0
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000058c8 0x00000000000058c8  R      0x1000
  LOAD           0x0000000000006000 0x0000000000006000 0x0000000000006000
                 0x0000000000040a01 0x0000000000040a01  R E    0x1000
  LOAD           0x0000000000047000 0x0000000000047000 0x0000000000047000
                 0x000000000000d11c 0x000000000000d11c  R      0x1000
  LOAD           0x0000000000054e08 0x0000000000055e08 0x0000000000055e08
                 0x0000000000003260 0x0000000000003360  RW     0x1000
  DYNAMIC        0x0000000000057578 0x0000000000058578 0x0000000000058578
                 0x0000000000000230 0x0000000000000230  RW     0x8
  NOTE           0x00000000000002fc 0x00000000000002fc 0x00000000000002fc
                 0x0000000000000044 0x0000000000000044  R      0x4
  TLS            0x0000000000054e08 0x0000000000055e08 0x0000000000055e08
                 0x0000000000000020 0x0000000000000060  R      0x8
  GNU_EH_FRAME   0x000000000004c53c 0x000000000004c53c 0x000000000004c53c
                 0x000000000000105c 0x000000000000105c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000054e08 0x0000000000055e08 0x0000000000055e08
                 0x00000000000031f8 0x00000000000031f8  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .plt.got .text .fini
   04     .rodata .debug_gdb_scripts .eh_frame_hdr .eh_frame .gcc_except_table
   05     .tdata .init_array .fini_array .data.rel.ro .dynamic .got .data .bss
   06     .dynamic
   07     .note.gnu.build-id .note.ABI-tag
   08     .tdata .tbss
   09     .eh_frame_hdr
   10
   11     .tdata .init_array .fini_array .data.rel.ro .dynamic .got

maru99maru99

dlopen, dlsymと動的リンカ

dlopen, dlsymを使用する場合と動的リンカを設定する場合の使い分けについて

1. 動的リンカを設定する

例えばコンパイルによって動的リンクを設定する場合、
コンパイルする環境と実行環境の両方に、その動的リンカのDT_NEEDEDに書かれているライブラリが必要になる。

*DT_NEEDEDは動的リンカによって読み込まれる共有ライブラリの情報が書かれている。動的リンカもEFLであり、Program Headerのdynamic Segmentの中に存在している。

2. dlopen, dlsym

dlopen dlsymを使用する場合、コンパイル環境に使用する共有ライブラリがなくてもコンパイルを行うことができる。
これによって、実行環境に応じて柔軟に共有ライブラリを変更することができる。

LD_PRELOAD

LD_PRELOADは環境変数であり、共有ライブラリの情報を設定しておく。
この環境変数を使用することで、動的リンカに設定されている共有ライブラリの読み込み時に割り込むことができる。

処理の流れとしては、

  1. 動的リンカが設定されている実行可能バイナリを実行する
  2. 設定されている動的リンカの解決が行われる
  3. 動的リンカのELF Headerの解決を行った後にProgram Headerの解決を行う。
  4. Program Headerのdynamic segmentに書かれている共有ライブラリの解決を行う。

この共有ライブラリの解決の先頭に割り込んで、LD_PRELOADに設定されている共有ライブラリの解決を行う。
基本的に、動的リンカのシンボル解決は先に設定されているものが優先されるので、LD_PRELOAD で割り込みを行うことで、共有ライブラリの実装を上書きすることができる。

これによって、mallocの実装をwrapしたり、__libc_start_mainの上書きを行うことができる。

maru99maru99

この機能を使用することで、mallocを差し替えてログを出したりするとこができる。
また、コンパイル済みバイナリのmain関数を置き換えたり、main関数の前に処理を追加することができる。

プログラム的にはmain関数が一番初めに実行されているように見えるが、実際にはその前に初期化処理を行なっている。 -> __libc_start_main

  1. コンパイル済みバイナリを実行すると、ELFのエントリポイントである_startが呼ばれる
  2. _startの中では最初に__libc_start_mainが実行される
  3. __libc_start_main実行後にユーザーのmain関数が実行される

*gnuではなくmuslを使用する場合、musl独自の__libc_start_mainが使用される。(muslは軽量版)

__libc_start_main

__libc_start_mainは以下のことを行なっている。

  1. 引数を受け取る
    • mainの関数ポインタやargc, argv, envpを渡される
  2. libcの内部初期化
    • errnoの設定やTLS(Thread Local Strage)の設定
maru99maru99

TLS(Thread Local Storage)について

大枠として、

1. プロセス

実行中のプログラムのまとまりのこと。
vimを開いたら1プロセス。firefoxを開いたら1プロセスになる。
各プロセスは独立したアドレス空間を持つ。

2. スレッド

プロセスの中で走る処理の流れ。
1プロセスの中には1つ以上のスレッドが存在し、最初のスレッドをメインスレッドという。
複数のスレッドは同じアドレス空間を共有する。


複数のスレッドが同じアドレス空間を共有することで、グローバル変数などを実現することができている。
しかし、各スレッドごとに独立した変数を持ちたくなることがある。

このときにTLS(Thrad Local Straga)を使用する。
TLSを使用することで、共有しているアドレス空間を仮想的に分割して、各スレッドごとにisolateすることができる。

maru99maru99

以下間違っている内容

Threadごとにメモリ領域の確保・分割を行う

以下の2つで内部処理が変わっている。

  1. Thread::spawnを使用するようなコードを実行する場合
  2. 各スレッドごとにグローバル変数を要するようなコードを実行する場合

1. thread::spawnを使用するコードを実行する場合

rustのthread::spawnを使用する場合、内部的にはlibcのpthread_createを使用している。
この関数は、内部的にmmapcloneシステムコールを実行している。

これによって、スレッドが増えるたびにスタック領域の確保が行われている。

mallocでも大きなメモリの確保ではmmapを使用している。今回のpthread_createでもmmapを使用しているので、ヒープ領域とスタック領域のメモリの違いについてわからなくなった。
実際のところ、ヒープ領域もスタック領域も同じメモリを使用している。違うのは管理の方法。
ヒープ領域はmallocで管理され、メモリのライフサイクルもmallocで管理されている。
それに対して、スタック領域はOSが管理している。
具体的には、threadが終了したときにメモリの解放が行われる

スレッド内部でグローバル変数を使用したい場合

このときにTLS(Thread Local Storage)の仕組みが使用される。

1. コンパイル時

TLSを使用するような変数を宣言した場合、コンパイラ・リンカによって、該当の実装はELFのPT_TLSセクションに配置される。

  • ファイルサイズ
  • メモリサイズ
  • 仮想アドレス
  • アラインメント制約

2. バイナリ実行時

PT_TLSに応じて、メインスレッド用のTLS領域を確保し、

maru99maru99

スレッドを生成する処理を実行する際のメモリ領域

例えば、rustでthreadを新たに生成するような処理だと、thread::spawn()を使用する。
この関数を実行したすると内部ではlibcのpthread_createを実行しており、pthread_createでは以下のことを行っている。

  1. mmap, cloneシステムコールを実行するして、thread用のスタック領域を確保する
  2. libcのmallocを読んで、各スレッドごとに存在するTLS用の領域を確保する

スレッドを生成する際には、各スレッドごとにスタックフレームが必要になるので、新たにスタック領域を確保する必要がある。

また、各スレッドごとに独立したグローバル変数を使用でいるようにするために、TLS用のメモリ領域を確保する必要がある。
この領域はサイズが大きくなく、いちいちmmapを使用してメモリ領域の確保を行なっていると無駄なので、mallocで確保しているメモリTLS用のメモリ領域を確保することが一般的である。