質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.37%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

Linux

Linuxは、Unixをベースにして開発されたオペレーティングシステムです。日本では「リナックス」と呼ばれています。 主にWebサーバやDNSサーバ、イントラネットなどのサーバ用OSとして利用されています。 上位500のスーパーコンピュータの90%以上はLinuxを使用しています。 携帯端末用のプラットフォームAndroidは、Linuxカーネル上に構築されています。

Q&A

解決済

2回答

5947閲覧

ELFファイルのレイアウトとメモリへの読み込まれ方がわからない

退会済みユーザー

退会済みユーザー

総合スコア0

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

Linux

Linuxは、Unixをベースにして開発されたオペレーティングシステムです。日本では「リナックス」と呼ばれています。 主にWebサーバやDNSサーバ、イントラネットなどのサーバ用OSとして利用されています。 上位500のスーパーコンピュータの90%以上はLinuxを使用しています。 携帯端末用のプラットフォームAndroidは、Linuxカーネル上に構築されています。

1グッド

2クリップ

投稿2018/05/15 16:19

ELFファイルのレイアウトとそのメンバについてわからないことがあります。

OS情報です。

Linux linux-mint 4.10.0-42-generic #46~16.04.1-Ubuntu SMP Mon Dec 4 15:57:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

以下は単純なCプログラム(main関数からsleep()を呼び出してスリープするだけです)からreadelf -l a.outでELFの情報を出したものです。

(図1)
イメージ説明

Program Headersのところには、プログラムヘッダの一覧が出ています。

lang

1typedef struct { 2 uint32_t p_type;  /* セグメントのタイプ */ 3 uint32_t p_flags; /* セグメントに関係するフラグ */ 4 Elf64_Off p_offset; /* ファイル先頭からセグメントまでのオフセット */ 5 Elf64_Addr p_vaddr; /* セグメントの最初のバイトが格納される仮想アドレス */ 6 Elf64_Addr p_paddr; /* セグメントの最初のバイトが格納される物理アドレス */ 7 uint64_t p_filesz; /* セグメントのファイルイメージサイズ */ 8 uint64_t p_memsz; /* メモリ上に配置されるセグメントのサイズ */ 9 uint64_t p_align; /* アライメント */ 10} Elf64_Phdr;

プログラムヘッダを表現する構造体は上記です。

また、先ほどのCプログラムを実行した時の/proc/pid/mapsの出力を見てみると以下のようになっています。

(図2 !! ASLRを無効化してコンパイルしてあります)
イメージ説明

わからないことの1つ目なのですが、Elf64_Phdrのメンバであるp_vaddrは、manページによるとセグメントの先頭バイトがある仮想アドレスを保持しているはずです。例えば、図1の上から4番目のLOADタイプのセグメントはDATAセグメントだと思われます。また、そのp_vaddrは0x0000000000600e10となっていますが、図2の3行目では開始アドレスは0x006010000となっており、ELFファイルが保持する情報と一致していないように思われます。

しかし、図1の上から3番目のLOADタイプのTEXTセグメントと思われるセグメントのp_vaddrは0x0000000000400000となっていますが、図2の一行目の開始アドレスは0x00400000となっており、こちらは一致しているように見えます。

これはなんなのでしょうか?

2つめですが、セグメントの位置と大きさについてです。図1にあるDATAセグメントの開始アドレスが0x0000000000600e10であり、 メモリ上のセグメントサイズを表すp_memszは0x230となっています。とすると、データセグメントの終了アドレスは以下のようになると思います。(1つめの疑問からいろいろと間違っているかもしれませんが、一応自分の頭の中で考えていることとして書きます)

0x0000000000601040 = 0x0000000000600e10 + 0x230

ですが、文字通りにとれば、DATAセグメントが0x0000000000601040で終わるとすると、その下のDYNAMICセグメントの開始アドレス0x0000000000600e28と被ってしまうはずです。

ELFはどのようにメモリにロードされているのでしょうか?

よろしくお願いします。

set0gut1👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

1つ目の質問ですが、実メモリ上のdataセグメントの開始アドレスは、ldで使われるデフォルトのリンカースクリプトの指定で並べ替えられているようです。こちらのブログの記事を読ませていただいて、分かり易かったので案内させていただきます。
ELF実行ファイルのメモリ配置はどのように決まるのか

私の環境(Ubuntu 17.04 64ビット)でも ld --verboseでリンカースクリプトを確認したところ、全内容の精査まではしていないものの、それっぽくなっていました。

以下の指摘事項ですが、

図2の3行目では開始アドレスは0x006010000となっており、ELFファイルが保持する情報と一致していない

(※0x00601000 の間違いですよね?)これはOracleのサイトでのELFの説明(ただし、Solaris/SunOS用)にヒントとなる記述がありました。
Program Header (Linker and Libraries Guide)

ここの「Base Address」の節で、以下の記述があります。

To compute the base address, you determine the memory address associated with the lowest p_vaddr value for a PT_LOAD segment. You then obtain the base address by truncating the memory address to the nearest multiple of the maximum page size. Depending on the kind of file being loaded into memory, the memory address might not match the p_vaddr values.

この文の後半、You then obtain...の文で「メモリアドレスの最大のページサイズの倍数に切り捨てることでベースアドレスを取得できる。メモリーにロードされたファイルの種類によっては、メモリーアドレスはp_vaddr の値に合致しない」とのことです。linuxのページサイズは4KiB(0x1000)なので、その倍数単位でベースアドレスになっていることに合致します。

また、android のlibcの代替であるCランタイムbionic のローダーのコード中の説明で、
linker/linker_phdr.cpp - platform/bionic

Finally, the real load addrs of each segment is not p_vaddr. Instead the
loader decides where to load the first segment, then will load all others
relative to the first one to respect the initial range layout.

との記述があります。いずれもlinux 64ビット版そのものではないですが、メモリ上にセグメントを割り当てる上でページサイズは意識せざるを得ないでしょうから、ELF形式の実行ファイルのローダーの挙動として参考になるかと思います。

まとめると、Elf64_Phdr->p_vaddr は 0x00600e10 なのに 実アドレス 0x00601000 からロードされるのは、ページサイズを意識した境界にローダーが配置する為で、更になぜ 0x00601000なのかと言えば、リンカースクリプトでそのように指定しているから、と言うことが言えるかと思います。

投稿2018/05/17 09:54

dodox86

総合スコア9254

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2018/05/17 12:19 編集

回答ありがとうございます。 やはり、アドレスの決定はリンカーが深く関与しているということですね。詳しく調べてみます。 ローダがどのように配置するか知りたく、いっそのことカーネルのコードを見てしまえと思ったので読んでみることにしました。リンカスクリプトも少しいじってみようと考えています。 ありがとうございました。
guest

0

読み込み方だけについてならば
TypeがLOADになってるこの場合2つだけ見ます。

0x006000000 - 0x006010000の属性が書き込み不可なのだけ引っかかりますが
後ろで宣言されてるGNU_RELROによって属性が上書きされてるのかもしれませんね

その下のDYNAMICセグメントの開始アドレス0x0000000000600e28と被ってしまうはずです。

FILEオフセットを計算すると同じ場所を示しています。

投稿2018/05/16 00:46

asm

総合スコア15149

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2018/05/16 02:16

回答ありがとうございます。 > FILEオフセットを計算すると同じ場所を示しています。 セグメントのp_offsetとp_vaddrはそれぞれファイル内で格納されている場所と実際にメモリにロードされる場所であり、それらは関係がない(お互いに影響を与えるものでない)と思っていたのですが、p_vaddrとp_offsetの関係はどうなっているのでしょうか? また、セグメントはPT_LOADタイプだけがメモリにロードされるということを思い出しました。自分でも混乱してよくわからないのですが、ではなぜ、ロードされないPT_NOTEやPHDRにもp_memszやp_vaddrなどの属性の値が指定してあるのでしょうか? リンカの都合でしょうか?
asm

2018/05/16 02:53

> それぞれファイル内で格納されている場所と実際にメモリにロードされる場所であり、それらは関係がない アドレス 0x0000000000600e28について LOADのVAから辿る場合: 0x0000000000600e10 + 0x18 DYNAMICのVAから〃〃: 0x0000000000600e28 + 0 ファイル上の位置では LOAD: 0xe10 + 0x18 = 0xe28 DYNA: 0xe28 + 0x00 = 0xe28 で同じ位置から読み込んでいます。 PT_LOADタイプで読み込んで、その他のタイプのセグメントによって用途をOS等に伝えている。 それだけだと思いますよ。 > p_memszやp_vaddr VAおよびファイルオフセットどちらからでもアクセスできるようにだと思いますよ 他のツールから特定セグメントのみ閲覧したい場合もありますし、PT_LOADの場合よりもわざわざ情報量を減らすメリットもなさそうです。
退会済みユーザー

退会済みユーザー

2018/05/16 03:20

ありがとうございました。 もう少し詳しく調べて見ます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.37%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問