セクションとセグメントの対応 part2
part1でセクションとセグメントを対応付けしていると思われる箇所は見つけた。次は実際にどうやって対応づけているのかを見てみる。
おさらい
簡単なプログラムをコンパイルして出来上がった実行ファイルはこんな感じになる。
$ readelf -l test.out Elf file type is EXEC (Executable file) Entry point 0x400340 There are 7 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000188 0x0000000000000188 R E 8 INTERP 0x00000000000001c8 0x00000000004001c8 0x00000000004001c8 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000354 0x0000000000000354 R E 200000 LOAD 0x0000000000000e98 0x0000000000600e98 0x0000000000600e98 0x0000000000000178 0x0000000000000178 RW 200000 DYNAMIC 0x0000000000000e98 0x0000000000600e98 0x0000000000600e98 0x0000000000000150 0x0000000000000150 RW 8 NOTE 0x00000000000001e4 0x00000000004001e4 0x00000000004001e4 0x0000000000000024 0x0000000000000024 R 4 GNU_RELRO 0x0000000000000e98 0x0000000000600e98 0x0000000000600e98 0x0000000000000168 0x0000000000000168 R 1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.gnu.build-id .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.plt .plt .text 03 .dynamic .got.plt 04 .dynamic 05 .note.gnu.build-id 06 .dynamic
上記の場合7つのプログラムヘッダーがあるが、セクションはそれぞれに分布し、また、複数セグメントにまたがる様に配置されているセクションもある。
実行可能ファイルを作成するときにはセクションを各セグメントへ振り分ける必要があるわけで、その振り分け方を調べてみる。
_bfd_elf_map_sections_to_segments()の調査
さて、part1で見つけたbinutilsのbfd/elf.cにあった_bfd_elf_map_sections_to_segments関数。まずはこれを見てみる。この関数だけを抜き出したのがこのコード。
PHDR、INTERPセグメント
まず74行目辺りを見ると
s = bfd_get_section_by_name (abfd, ".interp"); if (s != NULL && (s->flags & SEC_LOAD) != 0) {
ということで、セクション名が".interp"でSEC_LOADフラグを持っている場合は、このif文の中でPT_PHDRセグメントとPT_INTERPセグメントを作成している。翻せば、.interpセクションが無い場合はPHDRセグメントは不要と言うことか。
LOADセグメント
さて、最重要(だと思う)なLOADセグメント。このセグメントの作成は135行目から
for (i = 0, hdrpp = sections; i < count; i++, hdrpp++)
このループ内で行われる。そのちょっと後のコードを見た感じ、ループの初期化部で出てきてるsectionsが各セクションのアドレスを保持しているという事かな。sectionsを作ってる箇所は49行目からの
i = 0; for (s = abfd->sections; s != NULL; s = s->next) { if ((s->flags & SEC_ALLOC) != 0) { sections[i] = s; ++i; /* A wrapping section potentially clashes with header. */ if (((s->lma + s->size) & addr_mask) < (s->lma & addr_mask)) wrap_to = (s->lma + s->size) & addr_mask; } }
これ。つまりはSEC_ALLOCフラグを持ったセクションのみが集められてるということ。
145行目からしばらくは新しくセグメントを作るかどうかの判断。ここら辺はとりあえずスルーしておこう。新しくセグメントを作る必要が無い場合は223行目からのコードでフラグを調べてあれこれした後、234行目でcontinue。
新しくセグメントを作成した場合は240行目でmake_mappingとかってのをして、それに対してSEC_READONLYかどうかを見たりした上でアレコレ。このエントリの最初でreadelfより出したサンプルから、LOADセグメントは2つあってRW属性とRX属性の二種類があるのでそれらを分けてるのだろう。
260行目でこのループは終了。つまり、このループでSEC_ALLOCフラグを持ったセクションを、RW属性かRX属性どちらかの属性を持つLOADセグメントに振り分けていると。
もう一つのLOADセグメント
262行目のコメントを見ると、もう一つLOADセグメントを持つパターンもあるようだ。とりあえずスルー
DYNAMICセグメント
278行目からはDYNAMICセグメント。これについては
m = _bfd_elf_make_dynamic_segment (abfd, dynsec);
ということでDYNAMICセグメント作成用の関数が用意されていると。
NOTEセグメント
293行目からはNOTEセグメント。対象セクションは
if ((s->flags & SEC_LOAD) != 0 && CONST_STRNEQ (s->name, ".note"))
ということで、".note"がセクション名に含まれるものかな。readelfして出てきた結果では、".note.gnu.build-id"がNOTEセグメントに配置されていたし。
LOADセグメントの作成を掘り下げる
make_mapping関数
LOADセグメントの作成中に出てきたmake_mapping関数。これも同じくbfd/elf.cの中にあり、これを抜き出したコードはこれ。
コードを見ると、セグメントタイプとしてPT_LOADを指定してる所なんかを見たらもう、してやったりな気分ですね。
これで間違いあるまい。
SEC_ALLOC?
さて、ここまで見てみると、LOADセグメントへ配置される条件というのはセクションがSEC_ALLOCフラグを持つ事であることが読み取れる。そして、LOADセグメントのRX属性へ配置されるにはSEC_READONLYを持つ。そういえばこのSEC_ALLOCって何者か?セクションヘッダが持つ各値の定義を見ると、タイプの値はSHT_PORGBITSとかそんな感じだし、フラグはSHF_ALLOCとかそういうのだ(/usr/include/elf.hより)。
というわけでこのSEC_XXXXXを探してみると、bfd/bfd.hに定義がある。
抜粋すると
#define SEC_NO_FLAGS 0x000 /* Tells the OS to allocate space for this section when loading. This is clear for a section containing debug information only. */ #define SEC_ALLOC 0x001 /* Tells the OS to load the section from the file when loading. This is clear for a .bss section. */ #define SEC_LOAD 0x002 /* The section contains data still to be relocated, so there is some relocation information too. */ #define SEC_RELOC 0x004 /* A signal to the OS that the section contains read only data. */ #define SEC_READONLY 0x008 /* The section contains code only. */ #define SEC_CODE 0x010 /* The section contains data only. */ #define SEC_DATA 0x020 /* The section will reside in ROM. */ #define SEC_ROM 0x040
こんな感じ。以降、同様の定義が続いていく。
セクションとSEC_XXXXXとの関連
今までのをまとめると、各セクションにSEC_ALLOCを割り当てる基準があれば、それは即ちLOADセグメントへ含む指示を内包する事に他ならない。
ではその対応付けはどこでされているか?
ld/eelf_x86_64.cを調べてみると、gldelf_x86_64_place_orphan()という関数がある。コードはこんな感じ。
このコードにある以下のコード
static struct orphan_save hold[] = { { ".text", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_READONLY | SEC_CODE, 0, 0, 0, 0 }, { ".rodata", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_READONLY | SEC_DATA, 0, 0, 0, 0 }, { ".data", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_DATA, 0, 0, 0, 0 }, { ".bss", SEC_ALLOC, 0, 0, 0, 0 }, { 0, SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_READONLY | SEC_DATA, 0, 0, 0, 0 }, { ".interp", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_READONLY | SEC_DATA, 0, 0, 0, 0 }, { ".sdata", SEC_HAS_CONTENTS | SEC_ALLOC | SEC_LOAD | SEC_DATA | SEC_SMALL_DATA, 0, 0, 0, 0 }, { ".comment", SEC_HAS_CONTENTS, 0, 0, 0, 0 }, };
これか!
こうやって各セクション名に対してSEC_XXXXXというフラグ割り当てを定義してある。orphanというのは「みなしご」とか「親のない」という意味らしいので、つまりは最上位のセクション名について定義しているという事かな。
他のセクション名についても調べればあれこれ出てくるはず。というか、出てきた。
結論
part1での予想通り、ldの内部にどのセクションをどのセグメントへ配置するかの定義があった。
先述のstatic struct orphan_save hold[]を見ると、例えば
- .textセクション:SEC_ALLOC、SEC_READONLYがあるのでLOADセグメントのRX属性
- .dataセクション:SEC_ALLOCがあり、SEC_READONLYは無いのでLOADセグメントのRW属性
- .interpセクション:SEC_ALLOC、SEC_READONLYがあるのでLOADセグメントのRX属性
といった事がわかる。
.interpセクションについては、専用のセグメントを作成している箇所もあった。
セクションの順番指定についてはbfd/elf.cにelf_sort_sections()なんて関数があるので、これが整理してくれるのかな。