글
X86 32비트 어셈블리어 소스코드 예제를 통해서 TEXTREL 문제를 확인해보고, 이를 TEXTREL 문제 없는 어셈블리어 소스를 만들어보자. 먼저 다음과 같은 간단한 소스코드 예제를 보자.
#include <stdio.h> int main() { puts("Hello World\n"); return 0; }
이 소스를 clang으로 컴파일하고, objconv를 통해 NASM/YASM 소스코드로 변환시켜 조금 정리하면 다음과 같다. (clang -m32 -O2 -c 옵션으로 컴파일)
global main: function extern puts SECTION .text main: sub esp, 12 mov dword [esp], .str call puts xor eax, eax add esp, 12 ret ; main End of function SECTION .data .str: db "Hello World", 0xa, 0x0
스택 조작하는 부분을 더 간단히 고쳐보면 다음과 같다.
global main: function extern puts SECTION .text main: mov eax, .str push eax call puts add esp, 4 xor eax, eax ret SECTION .data .str: db "Hello World", 0xa, 0x0
이를 my.asm
로 저장하고 yasm -fnasm my.asm
으로 컴파일 후에 gcc -m32 my.o
명령으로 링크를 하면 실행이 잘 됨을 확인할 수 있다
인라인 어셈블리에 익숙해지려면 AT&T 스타일의 어셈블리어도 알아할 것이므로 -S 옵션으로 어셈블리어 소스를 얻고, 이를 정리해보면 다음과 같다.
.text .globl main .align 16 main: movl $.str, %eax push %eax calll puts addl $4, %esp xorl %eax, %eax retl .section .data .align 16 .str: .asciz "Hello World"
일대일 대응이므로 이해하는데 어렵지 않을 것이다. 이 소스는 my2.s로 저장한 후에 gcc -m32 my2.s로 컴파일하면 실행이 잘 됨을 확인할 수 있다.
이렇게 얻어진 어셈블리어 소스를 오브젝트 파일로 만든 후에 공유 라이브러리를 만들면 어떻게 될까?
다음과 같은 명령으로 공유라이브러리를 얻을 수 있으며, TEXREL 문제 역시 확인할 수 있다.
$ gcc -Wl,--warn-shared-textrel -m32 -shared -Wl,-soname,my2.so -o /tmp/my2.so my.o /usr/bin/ld: my.o: warning: relocation in readonly section `.text' /usr/bin/ld: warning: creating a DT_TEXTREL in a shared object.
그렇다면 이와 같은 문제의 어셈블리어 소스의 TEXTREL 문제를 어떻게 해결할 수 있을까?
함수의 리로케이션
우선 objdump -r 명령을 통해 리로케이션 정보를 살펴보자. (readelf -r 명령과 거의 동일)
$ objdump -r my.o my.o: file format elf32-i386 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 00000001 R_386_32 .data 00000007 R_386_PC32 puts
puts 함수의 리로케이션 정보를 고치기 위해서는 NASM의 경우 메뉴얼에 나온것처럼 다음과 같이 고치면 함수의 리로케이션 문제가 해결된다. 즉 함수 호출을 call puts
로 하는 대신에 call puts wrt ..plt
라고 해준다.
global main: function
extern puts
SECTION .text
main:
mov eax, .str
push eax
call puts wrt ..plt
add esp, 4
xor eax, eax
ret
SECTION .data
.str:
db "Hello World", 0xa, 0x0
이렇게 고친 후에 yasm -f elf my.asm으로 컴파일하고, objdump -r 명령으로 확인해보면 뭔가 달라졌음을 확인할 수 있다.
$ objdump -r my.o my.o: file format elf32-i386 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 00000001 R_386_32 .data 00000007 R_386_PLT32 puts
즉, 리로케이션 테이블에 있는 puts 엔트리의 오프셋은 전혀 변화가 없는데, 리로케이션 타입이 R_386_PLT32
로 바뀌었으며, 함수 puts에 대한 리로케이션 문제는 수정된 것이다.
AT&T 스타일의 소스의 경우는 다음과 같은 식으로 고쳐야 한다.
.text
.globl main
.align 16
main:
movl $.str, %eax
push %eax
calll puts@PLT
addl $4, %esp
xorl %eax, %eax
retl
.section .data
.align 16
.str:
.asciz "Hello World"
YASM/NASM에서는 조금 헷갈리게 만드는 wrt ..plt
라는 지시자를 썼지만, AT&T 스타일에서는 왠지 한결 쉬운듯한 @PLT
를 사용하고 있다. 이 경우 역시 gcc -m32 -c 명령으로 컴파일한 후에 gcc로 공유 라이브러리를 생성시켜보면 여전히 TEXTREL문제가 남아있음을 확인할 수 있는데, 이는 함수에 대한 리로케이션 뿐만 아니라, 데이터에 대한 리로케이션 정보도 바꿔줘야 하기 때문이다.
어셈블리어 소스는 @PLT 지시자를 통해서 고쳤으나, objdump -d를 통해서 바이너리 코드를 살펴보면 바뀌기 전 코드와 완전히 일치한다. @PLT같은 지시자는 리로케이션 테이블의 정보를 바꿔주는 역할을 할 뿐, 실제 실행 코드 (.text 섹션내 실행 코드) 자체는 변화가 없다.
리로케이션 테이블을 readelf -r 명령을 통해서 보면 다음과 같다.
$ readelf -r my.o Relocation section '.rel.text' at offset 0x54 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000001 00000201 R_386_32 00000000 .data 00000007 00000604 R_386_PLT32 00000000 puts
오브젝트 파일을 xxd로 덤프해서 바이너리 파일을 비교해보면 다음과 같은 차이가 있는 것을 볼 수 있다. (아래쪽 변경점은 소스파일의 파일 이름이 fa.asm 에서 fb.asm으로 변한 정보)
--- a 2017-03-10 18:54:40.016601488 +0900 +++ b 2017-03-10 18:54:43.364574036 +0900 @@ -4,11 +4,11 @@ 00000030: 0700 0100 0000 0000 0000 0000 0000 0000 ................ 00000040: b800 0000 0050 e8fc ffff ff83 c404 31c0 .....P........1. 00000050: c300 0000 0100 0000 0102 0000 0700 0000 ................ -00000060: 0406 0000 4865 6c6c 6f20 576f 726c 6421 ....Hello World! +00000060: 0206 0000 4865 6c6c 6f20 576f 726c 6421 ....Hello World! 00000070: 0a00 0000 002e 7465 7874 002e 6461 7461 ......text..data 00000080: 002e 7265 6c2e 7465 7874 002e 7374 7274 ..rel.text..strt 00000090: 6162 002e 7379 6d74 6162 002e 7368 7374 ab..symtab..shst -000000a0: 7274 6162 0000 0000 0066 612e 6173 6d00 rtab.....fa.asm. +000000a0: 7274 6162 0000 0000 0066 622e 6173 6d00 rtab.....fb.asm. 000000b0: 6d61 696e 0070 7574 7300 0000 0000 0000 main.puts....... 000000c0: 0000 0000 0000 0000 0000 0000 0100 0000 ................ 000000d0: 0000 0000 0000 0000 0400 f1ff 0000 0000 ................
데이터 리로케이션
다음은 데이터 리로케이션 정보를 넣어줄 차례이다. 그런데 이 경우는 x86_32비트의 경우 문제가 약간 복잡한데, 이유는 차차 설명하기로 한다.
먼저 64비트의 경우를 살펴보자. hello world 프로그램을 clang -O2 -S 옵션으로 소스를 얻고 정리를 해보면 다음과 같다.
.text .globl main main: movl $.str, %edi callq puts xorl %eax, %eax retq .section .rodata .str: .asciz "Hello World"
여기서 movl $.str, %edi
코드는 .str 문자열의 주소값($.str)을 %edi 레지스터에 넣으라는 것이다. 이 명령은 leal .str, %edi
와 하는 일이 같다.
64비트의 경우에는 함수 호출 규약이 더 간단한데, 스택으로 호출 인자를 넘기는 것이 아니라 rdi, rsi 등의 레지스터를 통해 인자를 넘기고 있다. 따라서 push 혹은 pop을 통해 인자를 넘기지 않고 곧바로 edi, esi 레지스터를 수정하게 된다.
이 프로그램의 리로케이션 문제를 해결하려 하면 다음과 같이 바꿔주어야 한다.
.text .globl main main: leal .str(%rip), %edi callq puts@PLT xorl %eax, %eax retq .section .rodata .str: .asciz "Hello World"
32비트에서처럼 함수의 경우에는 @PLT 지시자를 넣었으나, 데이터 섹션의 .str 주소를 넘기는 부분은 leal .str(%rip), %edi
를 사용했다.
이것은 단순히 지시자를 사용한 것이 아니며, X86_64(AMD64)에서 새로 도입된 레지스터 %rip를 사용해서 .str 문자열 주소를 넘겨준 것이다.
AT&T의 문법이 헷갈릴 수 있으니 이에 대응하는 YASM/NASM 어셈블리어로 살펴보자.
default rel global main extern puts SECTION .text main: lea edi, [rel .str] call puts wrt ..plt xor eax, eax ret SECTION .rodata .str: db "Hello World"
default rel
등등이 포함된 것을 제외하고는 32비트/64비트 프로그램 소스가 거의 같고, .str 문자열 주소를 넘겨주는 부분이 다른 것을 볼 수 있다.
문자열의 주소는 .str이며, .str의 주소의 내용은 [.str]인데, lea eax, [.str]
이라고 쓰면 .str의 주소값을 넘겨주라는 것이다. (NASM/YASM에서 사용할 수 있는 몇몇 주소 지정 방식은 다음 링크를 통해서 볼 수 있다. https://www.tortall.net/projects/yasm/manual/html/nasm-effaddr.html#nasm-effaddr-riprel)
이렇게 문자열 .str의 주소를 넘겨주는 코드에 rel
이라는 지시자를 넣으면 리로케이션 문제가 없는 오브젝트 파일이 생성된다. (NASM/YASM에서는 rip 레지스터를 직접 사용하면 오류가난다.) 이렇게 해서 생성된 코드의 바이트수도 조금 다르다는 것을 다음과 같이 확인할 수 있다.
$ objdump -d nopic.o ffa-nopic.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: 8d 3c 25 00 00 00 00 lea 0x0,%edi 7: e8 00 00 00 00 callq c <main+0xc> c: 31 c0 xor %eax,%eax e: c3 retq $ objdump -d pic.o # 수정된 PIC 코드의 경우 ffa.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: 8d 3d 00 00 00 00 lea 0x0(%rip),%edi # 6 <main+0x6> 6: e8 00 00 00 00 callq b <main+0xb> b: 31 c0 xor %eax,%eax d: c3 retq
함수와는 다르게, 데이터의 주소를 상대적 주소로 바꿔주기 위해서 명령 코드 자체가 조금 바뀐 것을 알 수 있는데, 64비트에서 데이터 주소를 상대적 주소로 바꿔주기 위해서 %rip 레지스터를 사용한다. %rip 레지스터는 X86_32에서는 %eip이며, 16비트 32비트의 경우 %eip 어드레스의 값을 손쉽게 불러오지 못하였으나, X86_64(AMD64)부터는 %rip를 불러서 상대적 주소를 쉽게 계산할 수 있게 된 것이다.
그렇다면 X86 32비트에서는 데이터의 상대주소를 어떻게 구할까?
Call Pop 트릭
콜팝은 콜라+팝콘의 뜻이 아니고, 32비트에서 %eip 레지스터의 값을 가져오는 트릭이다. call + pop을 통해 %eip 레지스터 값을 읽는 방식을 구현할 수 있다. 32비트에서도 명령 카운트 레지스터가 존재하지만, 이것은 직접 읽기가 불가능한 레지스터이다. 그런데 함수를 콜하게 되면 스택에 리턴할 주소를 저장한다는 사실을 알고 있다. 스택에 저장되는 리턴할 주소가 바로 명령 카운터 레지스터 %eip의 값인 셈이다. 또한, 리턴할 주소는 바로 다음 명령이 실행될 위치이며, 이 경우 local:
주소의 값이 된다. 따라서 아래와 같이 하면 %ebx 레지스터에 local:
의 실제 주소값이 저장될 것이다.
call local local: pop %ebx
pop 대신에 mov (esp), %ebx라고 해도 된다. 그렇다면 이렇게 얻어진 %ebx 값은 local:
의 실제 메모리에 올라간 진짜 주소가 된다는 것인데, 그렇다면 데이터가 적재된 곳의 주소 위치는 어떻게 구하게 될까? 64비트에서 lea .str(%rip), %eax라고 하면 되었듯이, %rip 대신에 %eip가 저장된 위치인 %ebx를 사용하여 leal .str(%ebx), %eax
라고 하면 될 것 같은데, 실제로는 이런 메커니즘을 지원하지 않으며, 다음과 같은 조금 아리송한 코드가 실제 사용된다. 즉,
call local local: pop %ebx leal .str(%ebx), %eax
라고 해야 하는 것이 아니라 다음과 같이 한다
call local local: pop %ebx local2: addl $_GLOBAL_OFFSET_TABLE_ + (local2 - local), %ebx leal .str@GOTOFF(%ebx), %eax
알쏭달쏭한 이 코드를 gdb로 파헤쳐보면 내막을 알 수 있지만, 실제로 _GLOBAL_OFFSET_TABLE_ 이라는 주소의 값은 프로그램이 적재되기 이전까지 알 수 없는 값이고, 소스코드상의 _GLOBAL_OFFSET_TABLE_은 조금 다르게 해석 되어진다. (이 부분은 아래에서 살펴보겠다.) 소스를 컴파일해서 얻어진 오브젝트 파일은 $_GLOBAL_OFFSET_TABLE_값을 아직 알지 못하고, 최종 실행파일은 링커에 의해 해석되어진 후에 GLOBAL OFFSET TABLE이 만들어지게 되어 비로소 GLOBAL_OFFSET_TABLE을 알게 된다.
본인도 이 부분이 제대로 이해되지 않아서 어려워했으나, gdb를 통해 내막을 살펴보고서야 이해하게 되었다.
_GLOBAL_OFFSET_TABLE은 변수? 상수?
우선 다음의 소스코드를 살펴보자. 이것은 hello world 프로그램의 PIC 버전이다. clang -O2 -fPIC -m32 -S 옵션으로 컴파일해서 소스를 얻고 이를 정리한 것이다.
.text .globl main main: call .tmp0 .tmp0: popl %ebx .tmp1: addl $_GLOBAL_OFFSET_TABLE_+(.tmp1-.tmp0), %ebx leal .str@GOTOFF(%ebx), %eax movl $_GLOBAL_OFFSET_TABLE_, %ebx ; 디버그용 코드 movl $_GLOBAL_OFFSET_TABLE_, %ebx ; 디버그용 코드 push %eax call puts@PLT add $4, %esp xorl %eax, %eax retl .section .rodata .str: .asciz "Hello World"
$_GLOBAL_OFFSET_TABLE값을 알기 위해서 중간에 디버그 코드를 넣었다. 이를 컴파일해서 오브젝트 파일 및 실행파일을 얻었다. 이를 objdump -d 명령으로 살펴보면 main 함수가 다음과 같다.
objdump -d 목적파일
00000000 <main>: 0: e8 00 00 00 00 call 5 <.tmp0> 00000005 <.tmp0>: 5: 5b pop %ebx 00000006 <.tmp1>: 6: 81 c3 03 00 00 00 add $0x3,%ebx c: 8d 83 00 00 00 00 lea 0x0(%ebx),%eax 12: bb 01 00 00 00 mov $0x1,%ebx 17: bb 01 00 00 00 mov $0x1,%ebx 1c: 50 push %eax 1d: e8 fc ff ff ff call 1e <.tmp1+0x18> 22: 83 c4 04 add $0x4,%esp 25: 31 c0 xor %eax,%eax 27: c3 ret
위의 내용을 잘 살펴보면 $_GLOBAL_OFFSET_TABLE_
값이 들어가야 할 자리가 $0x1
이라는 값이 들어있다. 또한, $_GLOBAL_OFFSET_TABLE_+(.tmp1-.tmp0)
가 들어가야 할 자리는 두 주소의 차이 (.tmp1-.tmp0)
값이 1이므로 0x02이어야 할 것 같은데 이상하게 $0x03이 들어있다. 그러면 실행파일을 덤프해보면 어떤가 살펴보자.
objdump -d 실행파일
...(생략) 0804840b <main>: 804840b: e8 00 00 00 00 call 8048410 <.tmp0> 08048410 <.tmp0>: 8048410: 5b pop %ebx 08048411 <.tmp1>: 8048411: 81 c3 f0 1b 00 00 add $0x1bf0,%ebx 8048417: 8d 83 c0 e4 ff ff lea -0x1b40(%ebx),%eax 804841d: bb e3 1b 00 00 mov $0x1be3,%ebx 8048422: bb de 1b 00 00 mov $0x1bde,%ebx 8048427: 50 push %eax 8048428: e8 b3 fe ff ff call 80482e0 <puts@plt> 804842d: 83 c4 04 add $0x4,%esp 8048430: 31 c0 xor %eax,%eax 8048432: c3 ret ...(생략) Disassembly of section .got.plt: 0804a000 <_GLOBAL_OFFSET_TABLE_>: 804a000: 14 9f adc $0x9f,%al 804a002: 04 08 add $0x8,%al
실행파일에서는 $_GLOBAL_OFFSET_TABLE_
이 들어갈 위치의 값이 모두 다른 값으로 채워져 있으며, $_GLOBAL_OFFSET_TABLE_+(.tmp1-.tmp0)
값에는 $0x1bf0(7152)
가 들어가고, $_GLOBAL_OFFSET_TABLE_
값이 들어갈 줄 알았던 곳은 각각 $0x1be3(7139)
, $0x1bde(7134)
값이 채워져 있다. 그리고 정작 _GLOBAL_OFFSET_TABLE_
주소는 0x0804a000
값이다.
실제로 상수인줄 알았던 소스코드상에 들어간 $_GLOBAL_OFFSET_TABLE_
값은 링커에 의해 고정된 값으로 해석되는 것이 아니라 .data
섹션과의 오프셋값으로 해석되어지고 있는 것이다.
따라서 실제 GLOBAL_OFFSET_TABLE
즉 실행파일이 메모리에 올라갔을 때에 .data 세그먼트의 위치를 가리키는 값은 다음과 같이 계산되어진다.
GLOBAL_OFFSET_TABLE의 실제 위치 = 현재 실행 코드의 %eip + 현재 실행 코드와 GLOBAL_OFFSET_TABLE 주소와 오프셋(여기서는 $_GLOBAL_OFFSET_TABLE_)
아무튼 이 과정은 GNU as + GND ld의 합작 과정인 것인데, YASM/NASM에서는 이마져도 약간 다르게 보이지만 원리는 같다. 다음 코드는 YASM/NASM에서 GLOBAL_OFFSET_TABLE
를 구하는 코드이다.
global main: function extern puts extern _GLOBAL_OFFSET_TABLE_ ; YASM/NASM에서는 이를 선언해주어야 한다. SECTION .text main: call .get_got .get_got: pop ebx .tmp: add ebx, _GLOBAL_OFFSET_TABLE_ + .tmp - .get_got ; AT&T 스타일의 어셈블리어와 같은 방식 ;add ebx, _GLOBAL_OFFSET_TABLE_ + $$ - .get_got wrt ..gotpc ; NASM/YASM 메뉴얼에 나와있는 방식. lea eax, [.str + ebx wrt ..gotoff] push eax call puts wrt ..plt add esp, 4 xor eax, eax ret SECTION .data .str: db "Hello World", 0xa, 0x0
이렇게 하여 이 소스를 컴파일하여 오브젝트 파일의 리로케이션 정보를 보면 다음과 같게 되며 TEXTRELs 문제가 없게 된다.
$ objdump -r test.o test.o: file format elf32-i386 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 00000008 R_386_GOTPC _GLOBAL_OFFSET_TABLE_ 0000000e R_386_GOTOFF .data 00000014 R_386_PLT32 puts
※참고:
http://nullprogram.com/blog/2016/12/23/ - GLOBAL_OFFSET_TABLE 및 x86에서 리로케이션에 대한 설명이 되어있다.
'리눅스 > 에셈블리어' 카테고리의 다른 글
mpg123의 x86상에서 text relocation 문제해결 (0) | 2017.03.14 |
---|---|
x86 공유 라이브러리의 TEXTRELS 오류 찾기 (0) | 2017.03.10 |
RECENT COMMENT