ELF64 Relocation 처리(2) - Relocation 수행 예제
원문 : http://kkamagui.springnote.com/pages/1722424
들어가기 전에...
0.시작하면서
이 문서는 ELF64 파일을 Relocation하는 데 필요한 정보가 기술된 문서입니다. 이 문서는 2부분으로 나누어져있으며, 첫번째 문서는 ELF64 파일 포맷에 대한 내용을 다루고 있고, 두번째 문서는 예제 코드를 통해 Relocation을 실제로 수행하는 내용을 담고 있습니다.
1.Relocation 예제
이제 실제로 Relocation을 수행해 보겠습니다. ^^ 첫번째 문서에서 설명드린 내용을 한번 훓어보셨다면 그리 어렵지 않습니다.
개발 환경은 32bit Windows이고 cygwin에서 64bit용 Cross Compiler를 만들어서 사용했습니다. 빌드로 나온 결과물이 별로 필요없는 정보를 포함하고 있어서, objcopy를 통해 .text, .data, .bss, .rodata Section과 연관된 Section만 추려서 ImageMaker.new라는 새로운 elf64 파일을 생성했습니다. ^^ 크게 중요한 부분은 아니니 그냥 실질적으로 사용하는 부분만 추렸다고 생각하시면 됩니다.
자~ 아래는 예제로 사용할 코드 및 make 파일 입니다.
- // ImageMaker.c 파일의 내용
- // Written by KKAMAGUI, http://kkamagui.tistory.com
-
- void A( int a );
int b = 5;
int c = 6;
int main( void )
{
A( 4 );
return 0;
}
void A( int a )
{
a = 3;
b = 4;
c = 5;
}
- # makefile의 내용
-
- # OS를 빌드하기위한 makefile
# Written by KKAMAGUI, http://kkamagui.tistory.com
all : ImageMaker.exe
ImageMaker.o : ImageMaker.c
x86_64-pc-linux-gcc -r -c $@ $<
ImageMaker.exe : ImageMaker.o
x86_64-pc-linux-ld -r -T elf_x86_64.x -nostdlib -o $@ $< -Ttext 0x000000
x86_64-pc-linux-objcopy -j .text -j .data -j .bss -j .rodata ImageMaker.exe ImageMaker.new
x86_64-pc-linux-objdump -x ImageMaker.exe > ImageMakerExeHeader.txt
x86_64-pc-linux-objdump -x ImageMaker.new > ImageMakerNewHeader.txt
x86_64-pc-linux-objdump -d ImageMaker.exe > ImageMakerExeDis.txt
x86_64-pc-linux-objdump -d ImageMaker.new > ImageMakerNewDis.txt
# 프로젝트 정리
clean :
rm -f ImageMaker.exe
rm -f ImageMaker.o
data :
x86_64-pc-linux-objcopy -j .text -j .data -j .bss -j .rodata ImageMaker.exe ImageMaker.new
x86_64-pc-linux-objdump -x ImageMaker.exe > ImageMakerExeHeader.txt
x86_64-pc-linux-objdump -x ImageMaker.new > ImageMakerNewHeader.txt
x86_64-pc-linux-objdump -d ImageMaker.exe > ImageMakerExeDis.txt
x86_64-pc-linux-objdump -d ImageMaker.new > ImageMakerNewDis.txt
clean 하실 경우 make clean을 입력하시면 되고, make또는 make data를 입력하시면 프로젝트를 빌드하거나, 빌드된 파일에서 Section 정보 및 디스어셈블리한 정보를 뽑아낼 수 있습니다. 다음 챕터에서 뽑아낸 정보를 분석해 보겠습니다. ^^
2.빌드된 실행 파일 분석
아래는 objcopy를 통해 필수요소만 추린 파일의 Section 정보 및 디스어셈블리 정보를 나타낸 것입니다. 빌드를 새로 하시거나, make data를 입력하면 아래의 두 파일을 최신 파일의 내용으로 갱신할 수 있습니다.
- ImageMakerNewHeader.txt 파일의 내용
ImageMaker.new: file format elf64-x86-64
ImageMaker.new
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000039 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000001000 0000000000001000 0000007c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000001008 0000000000001008 00000084 2**2
ALLOC
SYMBOL TABLE:
0000000000000000 l d .text 0000000000000000 .text
0000000000001000 l d .data 0000000000000000 .data
0000000000001008 l d .bss 0000000000000000 .bss
0000000000001000 g O .data 0000000000000004 b
0000000000000015 g F .text 0000000000000024 A
0000000000001004 g O .data 0000000000000004 c
0000000000000000 g F .text 0000000000000015 main
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
000000000000000a R_X86_64_PC32 A+0xfffffffffffffffc
0000000000000025 R_X86_64_PC32 b+0xfffffffffffffff8
000000000000002f R_X86_64_PC32 c+0xfffffffffffffff8
- ImageMakerNewDis.txt 파일의 내용
ImageMaker.new: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 04 00 00 00 mov $0x4,%edi
9: e8 00 00 00 00 callq e <main+0xe>
e: b8 00 00 00 00 mov $0x0,%eax
13: c9 leaveq
14: c3 retq
0000000000000015 <A>:
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 89 7d fc mov %edi,-0x4(%rbp)
1c: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%rbp)
23: c7 05 00 00 00 00 04 movl $0x4,0x0(%rip) # 2d <A+0x18>
2a: 00 00 00
2d: c7 05 00 00 00 00 05 movl $0x5,0x0(%rip) # 37 <A+0x22>
34: 00 00 00
37: c9 leaveq
38: c3 retq
자~ Section Header 정보와 디스어셈블리된 내용을 보니 뭔가 필이 오시나요? 잘 안오신다구요? 그렇다면 지금부터 차례차례로 살펴보겠습니다. ^^
3.Relocation 과정
우선 Section Header 정보에서 가장 아래쪽에 있는 Relocation 정보를 보겠습니다. 낮 익은 형태의 TYPE이 보이는 군요. 지난 문서에서 Relocation Type을 설명할때 R_X86_64_PC32에 대한 설명이 나왔습니다. 기억나시나요? ^^ R_X86_64_PC32는 Symbol의 Value와 r_addend을 더한 후, r_offset을 빼는 것으로 계산했습니다. 물론 계산 결과는 32bit 값입니다.
- RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
000000000000000a R_X86_64_PC32 A+0xfffffffffffffffc
0000000000000025 R_X86_64_PC32 b+0xfffffffffffffff8
000000000000002f R_X86_64_PC32 c+0xfffffffffffffff8
objdump는 친절하게도 Relocation Table Entry에서 r_info의 상위 32bit, 즉 Symbol Table Index를 검색해서 해당 Symbol을 위와같이 VALUE 항목에 표시해 줍니다. 실제로 우리가 직접 Section을 읽을 때는 r_info의 상위값으로 Symbol Table을 검색해야한다는 결론이지요. 이것은 실제로 분석할때 알아보고 지금은 objdump의 내용을 위주로 알아보겠습니다.
자~ 그럼 이제, Symbol Table의 값을 한번 볼까요? Symbol Table은 아래와 같이 구성되어있습니다. 값의 순서는 Symbol Value, Symbol Bind(Global, Local 등등), Symbol Type(Notype, Object(ex. 변수), Function 등), Symbol이 위치하는 Section, Symbol Size, Symbol Name입니다. Symbol Bind 및 Symbol Type은 크게 중요하지 않으므로 그냥 넘어가겠습니다(궁금하신 분들은 ELF 파일 포맷 문서를 참고하세요 ^^;;;).
- SYMBOL TABLE:
0000000000000000 l d .text 0000000000000000 .text
0000000000001000 l d .data 0000000000000000 .data
0000000000001008 l d .bss 0000000000000000 .bss
0000000000001000 g O .data 0000000000000004 b
0000000000000015 g F .text 0000000000000024 A
0000000000001004 g O .data 0000000000000004 c
0000000000000000 g F .text 0000000000000015 main
이 Symbol Table의 정보와 위에서 본 Relocation Entry의 값을 이용해서 실제 디스어셈브리된 결과에 적용시켜보겠습니다. Relocation Table Entry에 제일 먼저 나오는 Symbol A를 가지고 Relocation을 수행해 보겠습니다. 아래는 디스어셈브리된 코드에서 Symbol A를 사용하는 부분을 추린 것입니다.
- 0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 04 00 00 00 mov $0x4,%edi
9: e8 00 00 00 00 callq e <main+0xe>
e: b8 00 00 00 00 mov $0x0,%eax
13: c9 leaveq
14: c3 retq
0000000000000015 <A>:
15: 55 push %rbp
- ...... 생략 ......
원본 소스 코드인 ImageMaker.c를 보시면 아래와 같이 main() 함수에서 A(4)라고 호출하는 부분을 볼 수 있습니다.
- int main( void )
{
A( 4 );
return 0;
}
A(4)인 부분을 찾는다면 위에 디스어셈블리 된 결과에서 왼쪽에 나오는 Byte Offset 이 4인 부분과 9인 부분이 되겠습니다. 그럼 Byte Offset이 9인 부분을 같이 한번 보겠습니다. e8 00 00 00 00 으로 표기되어있군요. 그 우측에 나온 결과를 보면 main + 0xe의 위치를 호출하게 되어있습니다. 0xe의 위치는 call 명령이 끝난 바로 다음 위치를 나타낸다는 것을 알 수 있습니다.
Relocation Table Entry의 정보대로 한다면 R_X86_64_PC32 타입으로 계산해야 하므로 아래와 같이 계산되어 0x0000000000000007가 됩니다. 32bit 값으로 절삭해야되니 0x00000007이 되겠네요. ^^
Symbol Value + r_addend - r_offset = 15 + 0xfffffffffffffffc - 0x0a = 0x000000000000007
자 그럼 다시 디스어셈블리한 부분으로 돌아가겠습니다. Relocation Table Index의 첫번째 Entry에 따르면 Relocation이 수행되어야할 위치는 .text의 0xa 위치입니다. 0xa의 위치는 디스어셈블리 결과에서 Byte Offset이 9인 줄의 00으로 시작하는 위치가 되겠습니다. 즉 00 00 00 00의 위치에 우리가 계산한 결과인 0x7을 삽입하는 되는 겁니다.
- 9: e8 00 00 00 00 callq e <main+0xe>
만약 삽입을 했다면 callq의 결과는 어떻게 바뀔까요? 현재 전부 00으로 설정되어있을 때 함수를 호출하는 위치는 <main + 0xe>가 되고 call 명령 바로 다음 줄의 명령을 실행하게 됩니다. 하지만 여기에 0x07을 더해주면 함수를 호출하는 위치는 <main + 0xe + 0x7>, 즉 <main + 0x15> 가 되고, 이는 A 함수가 시작되는 위치의 Address이므로 A 함수가 불려지게 됩니다. 따라서 정상적으로 A 함수를 호출할 수 있습니다.
그런데 분명 call 명령 뒤에 Address값이 0x00000000으로 들어가있는데, 0x00000000에 있는 함수를 호출하는 것이 아니라 0xe의 명령을 호출할까요? 그것은 x86_64의 Address 계산 방식과 관계가 있습니다. x86은 64bit가 되면 기본적으로 Address를 계산할때 RIP, 즉 현재 수행 중인 명령의 다음 명령의 위치를 기반으로 값을 더해서 계산합니다. 따라서 위의 Byte Offset 이 9인 줄을 보면 0x00000000이 값으로 설정되있지만, RIP를 기반으로 해서 계산하면 다음 명령이 0xe에 위치하므로 0xe + 0x00000000이 되어 0xe가 되는 것입니다(Address 계산 방식에 대한 자세한 내용은 INTEL 문서나 기타 64bit에 대한 다른 문서를 보시면 잘 나와있습니다 ^^;;;;).