わらばんし仄聞記

南の国で引きこもってるWeb屋さん

セクションとセグメントの対応 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セグメントに配置されていたし。

その他のセグメント

更に進むと、341行目からはTLSセグメント。367行目からはGNU_EH_FRAMEセグメントと、いくつかのセグメントを作成している事が見て取れる。GNU_RELROセグメントなんかは重要そうだけど、とりあえず今回はパス。

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()なんて関数があるので、これが整理してくれるのかな。