Position Independent Executable と Shared Object を区別する判定
概要
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 を準備
素朴な疑問
上でつくった実行ファイル 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 つの違いはどこから来てるんだろうか?
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.out
も libc.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
file
の ソースコード + magic ファイルを見てみる
上記からすると、pie executable か shared object かはどうも ELF header 以外の判定があるようだが何で判定しているのかはよくわからない。
仕方がないので、file
のソースコードを見てみることにする (ソースコードは ここ から入手できる (URL が man file
から参照されている))。
file ./a.out
の出力からあたりをつけて pie executable
で grep
してみるとそれっぽいのがヒットした。
$ 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.out
と libc.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
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
の値は処理系の拡張で使われるような領域にあるっぽい感じである。
DT_FLAGS_1
と DF_1_PIE
の出どころを調べる
Dynamic section の内容で判定しているらしいというのはわかったので、「どこで pie executable と shared object を区別しているか」は一応わかったわけだが、なんだかよくわからない DT_FLAGS_1
やら DF_1_PIE
やら出てきて落ち着かないのでもうちょっとだけ調べたい。
DF_1_PIE
で google 検索してみると以下がヒットした。
こちらはどうやら golang の issue で、golang でビルドした position independent executable に対して DF_1_PIE
フラグを足すことで file
コマンドで正しく判定できるようにしようという issue のようだ。
issue からリンクをたどると以下の SO が出てくる。
なんというかこれもうほぼ今回の調査過程じゃんという気がしてくるが、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_1
や DF_1_PIE
については glibc
のほうの elf.h
で定義されていて、システムの /usr/include/elf.h
などに配置されているようだ。
.dynamic
セクションの確認
改めて 今回の調査で .dynamic
セクション内のフラグから pie と shared object を判定していることがわかったので、最後に改めて a.out
と libc.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
にはついていないのが確認できた。
めでたしめでたし