はじめに
malloc で確保したメモリに関して問題を解析する場合、切り売りしたメモリを管理する
malloc の main_arena の中身をダンプしたい時があります。
main_arena は、malloc.c の中で定義されている static な変数(ローカル変数)のため、
dlopen や dlsym を使ってシンボル情報を取得することはできません。
疑問
でも待てよっと、、、GDB は main_arena の存在、アドレスを知っていて、
アクセスすることもダンプすることもできています。なぜ?
GDB では ELF ファイルを解析して main_arena の存在やアドレスを知ることができています。
今回は「ELFを解析してmain_arenaのアドレスを取得する方法」について紹介いたします。
動作確認済みの環境
CPUアーキテクチャ | x86_64 |
OS | Ubuntu 20.04 LTE |
Linux Kernel | 5.4.0-122 |
gcc | 9.4.0 |
gdb | 9.2 |
glibc | 2.31 |
サンプルプログラム
各ポイントや処理内容の解説は後述することにして、まずはプログラム全体を記載いたします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
#define _GNU_SOURCE #include <link.h> #include <stdlib.h> #include <string.h> #include <stdio.h> #include <stdint.h> #include <dlfcn.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define DEBUG_SYMBOL_PATH_PREFIX "/usr/lib/debug/.build-id" #define DEBUG_SYMBOL_EXTENTION ".debug" #define TARGET_LIBRARY_PATH "/lib/x86_64-linux-gnu/libc.so.6" #define TARGET_SYMBOL_NAME "main_arena" static void *my_main_arena = NULL; static size_t map_file(const char *path, char **buf) { int fd = -1, ret; struct stat stat; *buf = MAP_FAILED; if ((fd = open(path, O_RDONLY)) < 0) return 0; ret = fstat(fd, &stat); if (ret) goto error; *buf = (char *)mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (*buf == MAP_FAILED) goto error; close(fd); return stat.st_size; error: if (fd != -1) close(fd); return 0; } static void unmap_file(void *buf, size_t size) { munmap(buf, size); } static void generate_symbol_file_path(char **path, const char *build_id, size_t size) { int i; char tmp[4]; char *p = *path; sprintf(p, "%s/%x/", DEBUG_SYMBOL_PATH_PREFIX, build_id[0]); build_id++; size--; for (i = 0; i < size; i++) { sprintf(tmp, "%02x", (unsigned char)build_id[i]); strcat(p, tmp); } strcat(p, DEBUG_SYMBOL_EXTENTION); printf("debug symbol file: path=%s\n", p); } static size_t find_symbol_offset(const char *symfile, const char *symname) { char *base = MAP_FAILED; size_t size; Elf64_Ehdr *header; Elf64_Shdr *secs; Elf64_Sym *symtab; char *names; int secidx, symidx; unsigned symcnt; size_t offset = (size_t)-1; size = map_file(symfile, &base); if (size == 0 || base == MAP_FAILED) goto error; header = (Elf64_Ehdr *)base; secs = (Elf64_Shdr *)(base + header->e_shoff); for (secidx = 0; secidx < header->e_shnum; secidx++) { if (secs[secidx].sh_type != SHT_SYMTAB) continue; if (secs[secidx].sh_entsize == 0) continue; symtab = (Elf64_Sym *)(base + secs[secidx].sh_offset); names = (char *)(base + secs[secs[secidx].sh_link].sh_offset); symcnt = secs[secidx].sh_size / secs[secidx].sh_entsize; for (symidx = 0; symidx < symcnt; symidx++) { if (strcmp(names + symtab[symidx].st_name, symname) == 0) { offset = symtab[symidx].st_value; goto final; } } } final: unmap_file(base, size); return offset; error: if (base != MAP_FAILED) unmap_file(base, size); return (size_t)-1; } static Elf64_Nhdr * find_build_id_note(const char *base) { Elf64_Ehdr *header = (Elf64_Ehdr *)base; Elf64_Shdr *secs = (Elf64_Shdr *)(base + header->e_shoff); Elf64_Nhdr *note = NULL; int secidx; char *name; for (secidx = 0; secidx < header->e_shnum; secidx++) { if (secs[secidx].sh_type != SHT_NOTE) continue; note = (Elf64_Nhdr *)(base + secs[secidx].sh_offset); if (note->n_type != NT_GNU_BUILD_ID) continue; name = (char *)(base + secs[secidx].sh_offset + sizeof(Elf64_Nhdr)); if (note->n_namesz == 4 && note->n_descsz != 0 && memcmp(name, "GNU", 4) == 0) { return note; } } return NULL; } int callback(struct dl_phdr_info *info, size_t size, void *data) { char *base = MAP_FAILED; size_t sz; Elf64_Nhdr *note; char *build_id; char *path = alloca(256); size_t offset; if (strcmp(info->dlpi_name, TARGET_LIBRARY_PATH) != 0) return 0; sz = map_file(info->dlpi_name, &base); if (sz == 0 || base == MAP_FAILED) return 0; note = find_build_id_note(base); if (!note) goto error; build_id = ((char *)note) + sizeof(Elf64_Nhdr) + note->n_namesz; generate_symbol_file_path(&path, build_id, note->n_descsz); offset = find_symbol_offset(path, TARGET_SYMBOL_NAME); if (offset == (size_t)-1) goto error; my_main_arena = (void *)(((char *)info->dlpi_addr) + offset); printf("%s found: %p\n", TARGET_SYMBOL_NAME, my_main_arena); unmap_file(base, sz); return 0; error: if (base != MAP_FAILED) unmap_file(base, sz); return 0; } int main(void) { dl_iterate_phdr(callback, NULL); return 0; } |
処理の流れ
サンプルプログラムは、以下のような処理の流れになっています。
step
1共有ライブラリのマップ情報取得
- 共有ライブラリのマップ情報を取得
dl_iterate_phdr は、現在マップ(ロード)されているライブラリのパス、
マップアドレス等を取得できます。個々の共有ライブラリごとに callback が呼ばれます。
step
2.note.gnu.build_id セクションを探す
- 実行バイナリとシンボル情報は別々のファイル
callback に渡される共有ライブラリや、実行時にマップしているライブラリを解析しても、
ローカル変数である main_arena の情報はどこにもありません。(hexdumpしても出てこない) - 別々のファイルを紐づける build_id
ライブラリのシンボル情報は、.note.gnu.build_id に書き込まれている build_id を参照して
格納されている場所を特定することができます。
メモ
build_id は、その名の通りビルドする際に付与される SHA1 の値で、
ビルドされたバイナリやライブラリ内の .note セクションに格納されます。
step
3シンボルファイルからmain_arenaのオフセット取得
- .symtab を解析
シンボルファイルの中には、最適化で消えていないシンボル情報が全て入っています。
.symtab セクションを解析すれば、所望のシンボル名が見つかります。 - シンボルファイルから取得できるのはオフセット
所望のシンボル名が st_name に見つかれば、st_value にオフセットが入っています。
共有ライブラリがマップされている dlpi_addr にオフセットを加算すれば、
アドレスを算出することができます。
実行結果
コンパイル
command
$ gcc -o test -g -O0 -Wall -Werror get_main_arena.c -lc -ldl
gdb で検証
command
$ gdb test
(gdb) b main
Breakpoint 1 at 0x1a7f: file get_main_arena.c, line 184.
(gdb) run
Starting program: /home/sanachan/test
Breakpoint 1, main () at get_main_arena.c:184
184 main(void) {
(gdb) n
185 dl_iterate_phdr(callback, NULL);
(gdb) n
debug symbol file: path=/usr/lib/debug/.build-id/18/78e6b475720c7c51969e69ab2d276fae6d1dee.debug
main_arena found: 0x7ffff7fb8b80
186 return 0;
(gdb) p/x &main_arena
$1 = 0x7ffff7fb8b80 ★glibc malloc.c 内の main_arena のアドレス
(gdb) p/x my_main_arena
$2 = 0x7ffff7fb8b80 ★ELFファイルを解析して得たアドレス
おわりに
今回は ELF ファイルを解析し、ローカル変数のアドレス取得する方法をご紹介いたしました。
x86_64 アーキテクチャ限定ですが、ELF ファイルのフォーマットはどのアーキテクチャでも同じですので、
異なるアーキテクチャへの展開時は、解析するセクションなどを合わせ込んでお使いください。