Closed7

Position Independent Executable と Shared Object を区別する判定

sankantsusankantsu

概要

3 行でまとめると以下

  • file コマンドで Position Independent Executable と Shared Object の表示結果が違うことに気づいた。
  • ELF header のレベルではどっちも ET_DYN で区別されていないっぽい
  • いろいろ調査した結果、.dynamic セクション内の DF_1_PIE フラグで区別しているのがわかった

せっかくなので、調査過程をぐだぐだと書いてみる。

環境

Apple Sillicon Mac 上に multipass で作成した aarch64 ubuntu VM 環境

環境の詳細
ubuntu@primary:~$ uname -a
Linux primary 6.8.0-41-generic #41-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug  2 23:26:06 UTC 2024 aarch64 aarch64 aarch64
GNU/Linux

ubuntu@primary:~$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

ubuntu@primary:~$ gcc --version
gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

ubuntu@primary:~$ ldd --version
ldd (Ubuntu GLIBC 2.39-0ubuntu8.3) 2.39
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

ubuntu@primary:~$ ld --version
GNU ld (GNU Binutils for Ubuntu) 2.42
Copyright (C) 2024 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.

準備

簡単な実行ファイル (hello world) の準備

// hello.c
#include <stdio.h>

int main() {
        printf("Hello, World!\n");
}
$ gcc hello.c  # 実行ファイル a.out を準備
sankantsusankantsu

素朴な疑問

上でつくった実行ファイル a.out について、file コマンドでファイルタイプを見てみると以下のように ELF 64-bit LSB pie executable と判定してくれる。

ubuntu@primary:~/code$ file ./a.out
./a.out: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linu
x-aarch64.so.1, BuildID[sha1]=b63fe22b8448f00e741d8aa0cc44a70f8e6344be, for GNU/Linux 3.7.0, not stripped

一方、共有ライブラリ (例えば libc.so.6) を file で見てみると、ELF 64-bit LSB shared object と判定される。

ubuntu@primary:~/code$ file /lib/aarch64-linux-gnu/libc.so.6
/lib/aarch64-linux-gnu/libc.so.6: ELF 64-bit LSB shared object, ARM aarch64, version 1 (GNU/Linux), dynamically lin
ked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=32fa4d6f3a8d5f430bdb7af2eb779470cd5ec7c2, for GNU/Linux
3.7.0, stripped

この 2 つの違いはどこから来てるんだろうか?

sankantsusankantsu

ELF header

まずは、ELF header で判定しているんじゃないかと思ったので、見てみる。

ubuntu@primary:~/code$ readelf --file-header ./a.out
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 (Position-Independent Executable file)
  Machine:                           AArch64
  Version:                           0x1
  Entry point address:               0x640
  Start of program headers:          64 (bytes into file)
  Start of section headers:          68512 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         28
  Section header string table index: 27

ubuntu@primary:~/code$ readelf --file-header /lib/aarch64-linux-gnu/libc.so.6
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           AArch64
  Version:                           0x1
  Entry point address:               0x286f0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          1719016 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         10
  Size of section headers:           64 (bytes)
  Number of section headers:         61
  Section header string table index: 60

Type のところはどっちも DYN になっていて、括弧内で file と同じような区別がある。

一応バイナリでも見てみるけれど、タイプを判定している e_type 17-18 バイト目の値は 3 (ET_DYN) になっていて、これは a.outlibc.so.6 も同じ

ubuntu@primary:~/code$ hexdump -C ./a.out | head -n 2
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  03 00 b7 00 01 00 00 00  40 06 00 00 00 00 00 00  |........@.......|

ubuntu@primary:~/code$ hexdump -C /lib/aarch64-linux-gnu/libc.so.6 | head -n 2
00000000  7f 45 4c 46 02 01 01 03  00 00 00 00 00 00 00 00  |.ELF............|
00000010  03 00 b7 00 01 00 00 00  f0 86 02 00 00 00 00 00  |................|

参考: https://www.sco.com/developers/gabi/2012-12-31/ch4.eheader.html

sankantsusankantsu

file の ソースコード + magic ファイルを見てみる

上記からすると、pie executable か shared object かはどうも ELF header 以外の判定があるようだが何で判定しているのかはよくわからない。
仕方がないので、file のソースコードを見てみることにする (ソースコードは ここ から入手できる (URL が man file から参照されている))。

file ./a.out の出力からあたりをつけて pie executablegrep してみるとそれっぽいのがヒットした。

$ grep -r 'pie executable'
./magic/Magdir/elf:>16  leshort         3               ${x?pie executable:shared object},

magic ってなんだと思ったが、どうやら file コマンドが利用する file magic (file 固有の識別子) をまとめたものらしい。
man magic もちゃんとある。
どうやら以下のような構造で並んでいるらしい。

<判定位置の byte offset> <判定位置にある型> <判定対象の値> <判定が成功した際のメッセージ>

やっぱり、(0-index) で ELF ファイルの 16 バイト目が 3 のときに pie executable か shared object と判定しているようである。
なんだか ${x?pie executable:shared object} という表記が気になるところで、3 項演算子っぽく見えるが残念ながら man magic にはドキュメントされていなさそうなのでまたソースコードに戻る。

いくらか読んだり grep していたりしたところ怪しげな部分が見つかった。

src$ grep -r '\${' *.c
./softmagic.c:  for (sptr = str; (ptr = strstr(sptr, "${")) != NULL;) {

中身を見てみると、varexpand() という関数で例の ${x?<str1>:<str2>} という文字列をパースしているようで、判定対象のファイルの実行パーミッション (0111) で結果を分岐しているように見えた。

`varexpand()` の中身
static int
varexpand(struct magic_set *ms, char *buf, size_t len, const char *str)
{
	const char *ptr, *sptr, *e, *t, *ee, *et;
	size_t l;

	for (sptr = str; (ptr = strstr(sptr, "${")) != NULL;) {
		l = CAST(size_t, ptr - sptr);
		if (l >= len)
			return -1;
		memcpy(buf, sptr, l);
		buf += l;
		len -= l;
		ptr += 2;
		if (!*ptr || ptr[1] != '?')
			return -1;
		for (et = t = ptr + 2; *et && *et != ':'; et++)
			continue;
		if (*et != ':')
			return -1;
		for (ee = e = et + 1; *ee && *ee != '}'; ee++)
			continue;
		if (*ee != '}')
			return -1;
		switch (*ptr) {
		case 'x':
			if (ms->mode & 0111) {
				ptr = t;
				l = et - t;
			} else {
				ptr = e;
				l = ee - e;
			}
			break;
		default:
			return -1;
		}
		if (l >= len)
			return -1;
		memcpy(buf, ptr, l);
		buf += l;
		len -= l;
		sptr = ee + 1;
	}

	l = strlen(sptr);
	if (l >= len)
		return -1;

	memcpy(buf, sptr, l);
	buf[l] = '\0';
	return 0;
}

しかしまだ疑問があって、a.outlibc.so.6 はどっちも +x のパーミッションがついてるので区別されないように思われる...

ubuntu@primary:~/code$ ls -l ./a.out /lib/aarch64-linux-gnu/libc.so.6
-rwxrwxr-x 1 ubuntu ubuntu   70304 Sep  5 00:47 ./a.out
-rwxr-xr-x 1 root   root   1722920 Aug  8 23:47 /lib/aarch64-linux-gnu/libc.so.6
sankantsusankantsu

もう少し file のソースを追う

ファイルモード (パーミッション) のようなもので区別されているのは間違い無さそうなので、関連しそうな部分を探してみると、readelf.c に以下の部分があった。

	switch (xdh_tag) {
	case DT_FLAGS_1:
		*pie = 1;
		if (xdh_val & DF_1_PIE)
			ms->mode |= 0111;
		else
			ms->mode &= ~0111;
		break;

なるほど、ファイルのパーミッションをそのまま使っているんじゃなくて、DF_1_PIE というフラグが立っていると 0111 をつけて、そうじゃなければリセットしているのか。
周辺を読んでみると、ELF ファイル内に Dynamic section (.dynamic) という動的リンクに関する情報が存在するときは、その中身を見てモードをつけ直しているらしい。

Dynamic section は以下のような構造体の配列になっている。

typedef struct {
	Elf64_Sxword	d_tag;
   	union {
   		Elf64_Xword	d_val;
   		Elf64_Addr	d_ptr;
	} d_un;
} Elf64_Dyn;

この中に d_tag = DT_FLAGS_1 で、d_val & DF_1_PIE = 1 を満たすようなものがあれば mode に 0111 を足して、そうじゃなければ消すという動きになっているようだ。

定数の定義は readelf.h にあった。

#define DT_FLAGS_1	0x6ffffffb	/* ELF dynamic flags */
#define	DF_1_PIE	0x08000000	/* Position Independent Executable */

しかし、generic ABI (gABI) にも processor specific ABI を見ても DT_FLAGS_1 なんていうものは見つからない。困った...

gABI をよく見ると、この DT_FLAGS_1 の値は処理系の拡張で使われるような領域にあるっぽい感じである。

sankantsusankantsu

DT_FLAGS_1DF_1_PIE の出どころを調べる

Dynamic section の内容で判定しているらしいというのはわかったので、「どこで pie executable と shared object を区別しているか」は一応わかったわけだが、なんだかよくわからない DT_FLAGS_1 やら DF_1_PIE やら出てきて落ち着かないのでもうちょっとだけ調べたい。

DF_1_PIE で google 検索してみると以下がヒットした。

https://github.com/golang/go/issues/46747

こちらはどうやら golang の issue で、golang でビルドした position independent executable に対して DF_1_PIE フラグを足すことで file コマンドで正しく判定できるようにしようという issue のようだ。
issue からリンクをたどると以下の SO が出てくる。

https://stackoverflow.com/questions/34519521/why-does-gcc-create-a-shared-object-instead-of-an-executable-binary-according-to/55704865#55704865

なんというかこれもうほぼ今回の調査過程じゃんという気がしてくるが、DT_FLAGS_1 やら DF_1_PIE は binutils による extension だと言っているので、最後におまけに binutils を見ておく。
ここ からソースを入手して見てみると、ld/ldelf.c に以下の箇所があった。

void
ldelf_after_parse (void)
{
  if (bfd_link_pie (&link_info))
    link_info.flags_1 |= (bfd_vma) DF_1_PIE;

なんだか詳細はよくわからないが、position independent executable としてリンクする場合には DF_1_PIE をつけているような雰囲気が感じられるのでこれで調査完了ということにしよう。
なお、DT_FLAGS_1DF_1_PIE については glibc のほうの elf.h で定義されていて、システムの /usr/include/elf.h などに配置されているようだ。

sankantsusankantsu

改めて .dynamic セクションの確認

今回の調査で .dynamic セクション内のフラグから pie と shared object を判定していることがわかったので、最後に改めて a.outlibc.so.6.dynamic フィールドを確認して締めとする。

ubuntu@primary:~/code$ readelf --dynamic ./a.out

Dynamic section at offset 0xfda0 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x5b8
(中略)
 0x000000006ffffffb (FLAGS_1)            Flags: NOW PIE
(略)

ubuntu@primary:~/code$ readelf --dynamic /lib/aarch64-linux-gnu/libc.so.6

Dynamic section at offset 0x19faf0 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-aarch64.so.1]
 0x000000000000000e (SONAME)             Library soname: [libc.so.6]
(中略)
 0x000000006ffffffb (FLAGS_1)            Flags: NOW
(略)

a.out には PIE フラグがついているが、libc.so.6 にはついていないのが確認できた。
めでたしめでたし

このスクラップは3ヶ月前にクローズされました