into the void

ソフトウェアに関する雑多な調査日記

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