読者です 読者をやめる 読者になる 読者になる

わらばんし仄聞記

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

セクションとセグメントの対応 part1

ELF binutils

前回ELFファイルを作る part4にて、セクションをセグメントに割り当てる際は単純な構造ということもあって決め打ちでやってみてた。前回の様な規模ならまあいいとして、これがある程度セクションの数や種類が増えてきたらどうしようもなくなる。
これについては通常リンク時に解決されるわけだが、それならばセクションとセグメントの対応はリンカスクリプトに書いてあるのか?と思ってみたわけだが、どうもそれらしい記述を見つけられなかった。しかし何処かに必ずその設定はあるはず。と、いうことで、その辺を追ってみた。

疑問

どのセクションをどのセグメントへ配置するかは、どのように決まっているのか?

予想

セクションを見たとき、そのセクションをどのセグメントへ割り当てるかを決める際に使用できそうな値は

  • セクションタイプ
  • フラグ(属性)
  • セクション名

がある。
基本的にはセクションタイプとフラグの組み合わせによって決まる部分が大きそうだが、それだと同じ組み合わせで異なるセグメントに配置と言うことに対応できなそう。
ということで、多分この三つともを使い、そのセクションがどのセグメントに配置されるかを決定してるのではないか。

調査

リンカスクリプトを調べる

セクションをセグメントへ配置する処理はリンク時に行われる。ということで、そのリンクの挙動を指定するリンカスクリプトを調べてみることに。前の記事でリンカスクリプトの場所にちょっと触れてたが、実際にldコマンドを実行したときにデフォルトで使用されているスクリプトについて見るには

$ ld --verbose

で得られる。

ここのページを読んでみた感じではPHDRSコマンドがあれば、どのセクションをどのセグメントに配置するかを指定できるようだが、上記の ld --verbose で得た結果にはPHDRSコマンドが見当たらなかった。
つまりはPHDRSに因らない決定方法があるはず。

では、どこで決めている?

このORACLEのページを見てみると、「エントランス基準」という項目に

リンカーは、定義済みのすべてのエントランス基準の内部リストを保持しています。

との記述がある。このドキュメントはsolarisのものだけど、基本的な所は大幅に違いはあるまい。多分・・・。
でも、このドキュメントに出てくるmapfileはlinuxについてググってみると、それらしいものは出てこないからsolaris用のものなのかな。そっちは色々出てくるし。

ということで、次に探す場所のヒントは得られた。リンカーが既にセクションとセグメントの配置を決める基準を保持していることがあるのだ。
ならば次に見るのはリンカそのもので決定。

ldコマンドを調べる

リンカといえばldなわけで、これのコードを調べてみればよい。ldコマンドはパッケージとしてはbinutilsに含まれるので、GNU binutilsのサイトからソースコードをDLしてくる。今回自分がDLして中身を見てみたのはbinutils-2.23.1。これを展開してみると、ldというディレクトリがある。まあこれがldコマンドの元になっているコードであることは間違い無いだろう。

さて、リンカとリンカスクリプトは当然関係があるはずなので、前回調べてみてたリンカスクリプトのパス(/usr/lib64/ldscripts/elf_x86_64.x)を元に、grepをかけてみて関連のありそうなファイルを探してみる。

$ grep -rl ldscripts /path/to/binutils/ld

結果を見てみて、怪しそうと思えた行は

./emultempl/elf32.em

コレ。これは怪しい。怪しすぎる。
さて、ここで改めてさっき出したデフォルトで使用しているリンカスクリプトを見直してみよう。すると、こんな一文がある。

/* .gnu.warning sections are handled specially by elf32.em.  */

なるほど。リンカスクリプトとの関連は間違いなさそうだ。まず調査する対象はこのファイルに決定。

elf32.emから見始めて

とはいえ、ちょっと疑問思ったのは自分の環境は64bitマシン。でも出てきたファイルはelf32.em。elf64.emは無いのかな?
ファイルを開いてみてすぐの3行目にこんな記述がある

# This file is now misnamed, because it supports both 32 bit and 64 bit

なるほどね・・・。とりあえず自分の環境でも調べるべきファイルはこれで良さそうだ。
さて、このemultempl/elf32.em。ディレクトリ名からも分かるように、実際にファイルの中身を見てみると関数名に

static char *                                                                     
gld${EMULATION_NAME}_add_sysroot (const char *path)

といったように ${EMULATION_NAME}といったような変数名と思われる箇所が散見される。このEMULATION_NAMEを検索してみると

$ grep -rl EMULATION_NAME . | grep -v ChangeLog | grep -v emultempl
...
./ld/genscripts.sh
...

というのが見つかる。なるほど、さっきのelf32.emはこのgenscripts.shで作成されるスクリプトのベースとなるスクリプトということね。
さて、genscripts.shの中身を見てEMULATION_NAMEの使われてる箇所を見てみると

( echo "/* Default linker script, for normal executables */"
  . ${CUSTOMIZER_SCRIPT}
  . ${srcdir}/scripttempl/${SCRIPT_NAME}.sc
) | sed -e '/^ *$/d;s/[     ]*$//' > ldscripts/${EMULATION_NAME}.x

とかって記述が。これからリンカスクリプトを生成するときにファイル名として使われていることがわかるので、自分の使っていたリンカスクリプトである"/usr/lib64/ldscripts/elf_x86_64.x"より、自分が今求めている対象はEMULATION_NAME = elf_x86_64となっていたことが読み取れる。

他にもelf32.emを見てると

1909   /* Decide which segment the section should go in based on the
1910      section name and section flags.  We put loadable .note sections
1911      right after the .interp section, so that the PT_NOTE segment is
1912      stored right after the program headers where the OS can read it
1913      in the first page.  */

なんて文があったりしていい感じ。
他のファイルにも手を伸ばしてみると、ld/emultempl/elf-genericl.em(このファイルがelf32.emと関係してるのはMakefileを見れば間違いなさそうなのは読み取れる)にある

_bfd_elf_map_sections_to_segments

なんて関数が名前からして雰囲気出てるので、関数の実装を見てみるべくbfd/elf.cをのぞいてみると、まさにこれだろうと言うような処理。elf-generic.emにあるループで1セクションずつbfd/elf.cの_bfd_elf_map_sections_to_segmentsへ渡して、適切なセグメントに割り振ってるっぽい。

なにはともあれ、これらがそのままコードとして使われるわけじゃなく、makeしたときに動的に生成される為の元ファイルということで。それに現状だとどうやってここにたどり着くのかよく分からん。ならばまずはmakeしてみる。binutilsを展開したディレクトリにて

$ cd /path/to/binutils
$ ./configure
$ make

installはしない。コード見たいだけだし。
そして出来上がるお目当てのファイル、ld/eelf_x86_64.c。

ld/eelf_x86_64.cから見始めて

中身を見てみると、早速お目当ての関数を発見。

64 static void
65 gldelf_x86_64_map_segments (bfd_boolean need_layout)
66 {
  ...
84       if (!_bfd_elf_map_sections_to_segments (link_info.output_bfd,
85                           &link_info))
86         einfo ("%F%P: map sections to segments failed: %E\n");

ここにたどり着くまでの足跡を辿ってみる。まずは同じファイル内に

1802 static void
1803 gldelf_x86_64_after_allocation (void)
1804 {
1805   bfd_boolean need_layout = bfd_elf_discard_info (link_info.output_bfd,
1806                           &link_info);
1807   gldelf_x86_64_map_segments (need_layout);
1808 }

という箇所で呼び出され、これまたこの関数は同じファイルにて

4801 struct ld_emulation_xfer_struct ld_elf_x86_64_emulation =
4802 {
4803   gldelf_x86_64_before_parse,
4804   syslib_default,
4805   hll_default,
4806   after_parse_default,
4807   gldelf_x86_64_after_open,
4808   gldelf_x86_64_after_allocation,

として定義されている。ld_elf_x86_64_emulationの使用箇所を探すと

$ grep -r ld_elf_x86_64_emulation .
./ldemul-list.h:extern ld_emulation_xfer_type ld_elf_x86_64_emulation;

とかって出てくるので、中を拝見する。

$ cat ldemul-list.h
/* This file is automatically generated.  DO NOT EDIT! */
extern ld_emulation_xfer_type ld_elf_x86_64_emulation;
extern ld_emulation_xfer_type ld_elf32_x86_64_emulation;
extern ld_emulation_xfer_type ld_elf_i386_emulation;
extern ld_emulation_xfer_type ld_i386linux_emulation;
extern ld_emulation_xfer_type ld_elf_l1om_emulation;
extern ld_emulation_xfer_type ld_elf_k1om_emulation;

#define EMULATION_LIST \
  &ld_elf_x86_64_emulation, \
  &ld_elf32_x86_64_emulation, \
  &ld_elf_i386_emulation, \
  &ld_i386linux_emulation, \
  &ld_elf_l1om_emulation, \
  &ld_elf_k1om_emulation, \
  0

エミュレーションリストとしてdefineされてる。次に探すはこのEMULATION_LISTを使ってる箇所

$ grep -r EMULATION_LIST .
./ldemul.c:ld_emulation_xfer_type *ld_emulations[] = { EMULATION_LIST };

このファイル内でld_emulationsを使っている箇所がいくつかあるので、ひとまず怪しげな箇所をピックアップ

276 void
277 ldemul_choose_mode (char *target)
278 {
279   ld_emulation_xfer_type **eptr = ld_emulations;
280   /* Ignore "gld" prefix.  */
281   if (target[0] == 'g' && target[1] == 'l' && target[2] == 'd')

この関数、ldemul_choose_modeを使ってる箇所を探すと

$ grep -r ldemul_choose_mode .
./ldmain.c:  ldemul_choose_mode (emulation);

きた!遂にmainきた!
中身を見てみると

189 int
190 main (int argc, char **argv)
191 {
192   char *emulation;
....
293   emulation = get_emulation (argc, argv);
294   ldemul_choose_mode (emulation);
295   default_target = ldemul_choose_target (argc, argv);

これだ。ちゃんとmain関数内で呼ばれてる。こうやってリンクするモードを選んだりして、適切な実行ファイルとかを出力してるのね。

振り返ってみる

元々はセクションをどうやってセグメントに関連付けてるのか、そのパターンを読もうとしてたのに何故か関連付けの処理自体を途中から追ってしまってた。
まあ、その関連付けの処理にたどり着くまではわかったので、そこの詳細はまた後でかな・・・。