ELF for ARM(リロケーションの仕組み)
ARMv7向けにコンパイルしたオブジェクトファイルを解析してリローケションの仕組みを調べてみた。
関数アドレスのリロケーション
外部関数の呼び出しを行っている箇所は、コンパイル時には関数のアドレスが不明。
そのため、リンク時までは具体的なアドレスは入れずに、再配置対象のシンボルとして管理しておく。
再配置処理は命令セットによってやり方が異なるため、ARMならARM用の、x86ならx86用、x64ならx64用の再配置処理が実装されている。
下記のシンプルなコードで再配置の様子を調べてみた。
対象コード
main.c
int main(void){ hello(); bye(); return 0; }
sub1.c
int gVal1 = 1; int gVal2 = 2; int gVal3 = 3; void hello(){ gVal2 = gVal1 + 1; } void bye(){ gVal3 = gVal3 + 1; }
コンパイルと逆アセンブル
ARM用のクロスコンパイラを使ってmain.cをコンパイル。
$ arm-linux-gnueabihf-gcc -c main.c
ARM用のオブジェクトファイルができていることを確認。
$ file ./main.o ./main.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped
objdumpで逆アセンブル。
$ arm-linux-gnueabihf-objdump -d main.o main.o: file format elf32-littlearm Disassembly of section .text: 00000000 <main>: 0: e92d4800 push {fp, lr} 4: e28db004 add fp, sp, #4 8: ebfffffe bl 0 <hello> c: ebfffffe bl 0 <bye> 10: e3a03000 mov r3, #0 14: e1a00003 mov r0, r3 18: e8bd8800 pop {fp, pc}
外部関数hello()とbye()を呼び出しているところに注目。
8: ebfffffe bl 0 <hello> c: ebfffffe bl 0 <bye>
ブランチ命令(bl)のオペランドが0
代わりに外部関数のシンボル名(hello, bye)が表示されている。
blのオペコードは0xeb。つづく3byteがジャンプ先のアドレスを示すオペランド。ここがhelloもbyeも0xfffffeになっている。
.rel.textセクションの解析
逆アセンブルした結果は、そこが再配置対象のコードで、そのシンボル名がhelloであるということが分かるように表示されている。
しかし、コード自体は、ebfffffeとなっているたけで、fffffeというオペコードから再配置対象らしいということは推測できるものの、helloというシンボルとの対応付けができるだけの情報は含まれていない。
ELF形式のオブジェクトファイルには命令コードが格納される.textの他に下記のセクションが格納されている。
$ arm-linux-gnueabihf-readelf -S ./main.o There are 11 section headers, starting at offset 0x138: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 00001c 00 AX 0 0 4 [ 2] .rel.text REL 00000000 0003cc 000010 08 9 1 4 [ 3] .data PROGBITS 00000000 000050 000000 00 WA 0 0 1 [ 4] .bss NOBITS 00000000 000050 000000 00 WA 0 0 1 [ 5] .comment PROGBITS 00000000 000050 00005c 01 MS 0 0 1 [ 6] .note.GNU-stack PROGBITS 00000000 0000ac 000000 00 0 0 1 [ 7] .ARM.attributes ARM_ATTRIBUTES 00000000 0000ac 000031 00 0 0 1 [ 8] .shstrtab STRTAB 00000000 0000dd 000059 00 0 0 1 [ 9] .symtab SYMTAB 00000000 0002f0 0000c0 10 10 9 4 [10] .strtab STRTAB 00000000 0003b0 00001a 00 0 0 1
再配置に関する情報は.text.relセクションにまとめられている。
readelfで.text.relセクションを参照するとこんな感じで表示される。
Relocation section '.rel.text' at offset 0x3cc contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000008 00000a1c R_ARM_CALL 00000000 hello 0000000c 00000b1c R_ARM_CALL 00000000 bye
オブジェクトファイルのバイナリは下記。0x3CCから。
000003C0 68 65 6C 6C 6F 00 62 79 65 00 00 00 08 00 00 00 1C 0A 00 00 hello.bye........... 000003D4 0C 00 00 00 1C 0B 00 00 ........
.text.relのフォーマットは下記。定義はbinutilsのソースコードに含まれるものを参照した。binutilsのソースは
http://ftp.gnu.org/gnu/binutils/からダウンロードできる。
include/elf/external.h
/* Relocation Entries */ typedef struct { unsigned char r_offset[4]; /* Location at which to apply the action */ unsigned char r_info[4]; /* index and type of relocation */ } Elf32_External_Rel;
前半4byteが再配置対象のオフセット。後半4byteが再配置のタイプと、シンボルのシンボルテーブル上の位置情報。
0x3CCから始まるエントリの場合、前半が0x08000000なので、対象オフセットが0x08。逆アセンブルされた命令コード部みると。
00000000 <main>: 0: e92d4800 push {fp, lr} 4: e28db004 add fp, sp, #4 8: ebfffffe bl 0 <hello> c: ebfffffe bl 0 <bye> 10: e3a03000 mov r3, #0 14: e1a00003 mov r0, r3 18: e8bd8800 pop {fp, pc}
たしかにオフセット0x08はhelloの呼び出し部。
後半が0x1C0A0000。1Cが再配置のタイプ。readelfの出力を信じればR_ARM_CALL。binutilsのソース上でも一致。
include/elf/arm.h
RELOC_NUMBER (R_ARM_PLT32, 27) /* deprecated - 32 bit PLT address. */ RELOC_NUMBER (R_ARM_CALL, 28) RELOC_NUMBER (R_ARM_JUMP24, 29)
0x0A0000がシンボルテーブル上の位置。
readelfでシンボルテーブルを表示すると
Symbol table '.symtab' contains 12 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS main.c 2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 SECTION LOCAL DEFAULT 4 5: 00000000 0 NOTYPE LOCAL DEFAULT 1 $a 6: 00000000 0 SECTION LOCAL DEFAULT 6 7: 00000000 0 SECTION LOCAL DEFAULT 5 8: 00000000 0 SECTION LOCAL DEFAULT 7 9: 00000000 28 FUNC GLOBAL DEFAULT 1 main 10: 00000000 0 NOTYPE GLOBAL DEFAULT UND hello 11: 00000000 0 NOTYPE GLOBAL DEFAULT UND bye
たしかに0x0A = 10のエントリにhelloの情報が登録されている。
ここまでで大体のからくりが分かった。
あと不思議なのが、再配置対象のオフセット0x08はbl命令のオフセット。0xebfffffeのどこをhelloのアドレスで置き換えればよいかの情報がここまでで出てきていない。
ヒントは再配置タイプのR_ARM_CALL。binutilsのソースの中を探してみたら、再配置タイプ別の処理定義をしている部分を発見。
bad/elf32-arm.c
HOWTO (R_ARM_CALL, /* type */ 2, /* rightshift */ 2, /* size (0 = byte, 1 = short, 2 = long) */ 24, /* bitsize */ TRUE, /* pc_relative */ 0, /* bitpos */ complain_overflow_signed,/* complain_on_overflow */ bfd_elf_generic_reloc, /* special_function */ "R_ARM_CALL", /* name */ FALSE, /* partial_inplace */ 0x00ffffff, /* src_mask */ 0x00ffffff, /* dst_mask */ TRUE), /* pcrel_offset */
src_mask、dst_maskで0xebfffffeのどこが書き換え対象を指定している模様。オペコードblのマシン語は0xebなので、そこを除く24bitをhelloのアドレスで再配置時に書き換えるものと思われる。
再配置後(リンク語)のコード
main.oとsub1.oをリンクしてmainという実行可能バイナリを作成。これを逆アセンブルした。
000083e4 <main>: 83e4: e92d4800 push {fp, lr} 83e8: e28db004 add fp, sp, #4 83ec: eb000003 bl 8400 <hello> 83f0: eb00000e bl 8430 <bye> 83f4: e3a03000 mov r3, #0 83f8: e1a00003 mov r0, r3 83fc: e8bd8800 pop {fp, pc} 00008400 <hello>: 8400: e52db004 push {fp} ; (str fp, [sp, #-4]!) 8404: e28db000 add fp, sp, #0 8408: e59f3018 ldr r3, [pc, #24] ; 8428 <hello+0x28> 840c: e5933000 ldr r3, [r3] 8410: e2832001 add r2, r3, #1 8414: e59f3010 ldr r3, [pc, #16] ; 842c <hello+0x2c> 8418: e5832000 str r2, [r3] 841c: e24bd000 sub sp, fp, #0 8420: e49db004 pop {fp} ; (ldr fp, [sp], #4) 8424: e12fff1e bx lr 8428: 000105f0 .word 0x000105f0 842c: 000105f4 .word 0x000105f4
main関数でhelloを呼び出していた部分が、0xebfffffeから0xeb000003に書き換わっている。
bl