- 작은 C
hello 프로그램도 Linux에서는 ELF 실행 파일이 되며, readelf, nm, objdump로 내부 구조를 직접 확인할 수 있음
- 실행 파일을 이해하는 핵심 축은 심볼, 섹션, 세그먼트이며, 각각 함수 연결·코드/데이터 구분·실행 시 메모리 배치를 담당함
objdump와 readelf를 쓰면 .text, .rodata, .data, .bss, .interp 같은 섹션의 바이트와 속성을 확인할 수 있음
- 프로그램은
main에서 바로 시작하지 않고 _start 로 진입한 뒤 여러 초기 작업을 거쳐 main을 호출함
- 실행 파일은 “읽을 수 없는 덩어리”가 아니라 정해진 형식의 파일이므로, 도구를 쓰면 코드·문자열·링킹 정보를 단계적으로 추적할 수 있음
실행 파일은 읽을 수 있는 파일 형식임
- 컴파일된 실행 파일은 처음에는 읽을 수 없는 “마법 같은 바이너리”처럼 보이지만, 실제로는 이해 가능한 파일 형식임
- 예시는 Linux의 ELF 바이너리를 대상으로 하며, 바이너리는 플랫폼 의존적이므로 설명도 플랫폼에 묶여 있음
- 사용한 예제는 다음 C 프로그램임
#include <stdio.h>
int main() {
printf("Penguin!\n");
}
gcc -o hello hello.c로 컴파일해 hello 실행 파일을 만든 뒤 내부를 살펴봄
- 전체 흐름은 세 가지 개념을 중심으로 이어짐
- 심볼(symbols):
printf처럼 다른 곳에 정의된 함수를 호출할 때 위치를 찾는 데 쓰임
- 섹션(sections): 코드와 데이터를 나누는 단위이며
.text, .data, .rodata 등이 있음
- 세그먼트(segments): 섹션들을 실행 시점의 메모리 배치 단위로 묶음
텍스트로 열어도 단서는 보임
cat hello처럼 실행 파일을 그대로 열면 대부분 깨진 문자처럼 출력됨
- 그래도 출력 안에서
Penguin!과 ELF 같은 문자열을 찾을 수 있음
- ELF는 이 바이너리의 파일 형식 이름임
- 출력 대부분을 사람이 읽기 어려운 이유는 실행 파일이 바이너리 데이터이기 때문임
심볼 테이블로 함수 이름과 연결을 확인함
readelf --symbols hello는 실행 파일의 심볼 테이블을 출력함
- 예제 출력에는 주요 심볼이 나타남
main: 작성한 main() 함수의 주소
puts@@GLIBC_2.2.5: 코드에서 호출한 printf와 관련된 참조로 보이며, 컴파일러가 최적화로 puts로 바꾼 것으로 추정됨
_start: 프로그램 시작과 관련된 중요한 심볼
- 프로그램은
main에서 바로 시작하지 않고 실제로는 _start 로 진입함
_start는 여러 중요한 작업을 수행하며, 그중 main 호출도 포함됨
심볼은 연결을 가능하게 함
- 프로그램에
hello라는 함수를 작성하면, 컴파일된 바이너리에서 해당 함수의 코드에 hello라는 심볼이 붙음
- 라이브러리 함수인
printf를 호출하려면 그 함수의 코드 위치를 찾을 방법이 필요함
- 함수 위치를 찾는 과정이 링킹(linking) 임
- 컴파일 직후에 일어나면 정적 링킹
- 프로그램 실행 시점에 일어나면 동적 링킹
libc에는 C 표준 라이브러리 함수들이 들어 있음
nm이 libc에서 “no symbols”를 출력해도 objdump -tT /lib/x86_64-linux-gnu/libc-2.15.so로 심볼을 볼 수 있음
- libc의 심볼 테이블에서는
sprintf, strlen, fork, exec 같은 함수들을 확인할 수 있음
hello가 puts를 호출하고 libc의 심볼 테이블에서 puts 위치를 찾는 흐름으로 동적 링킹의 동작을 상상할 수 있음
섹션은 코드와 데이터를 나눔
objdump -s hello는 실행 파일의 각 섹션에 들어 있는 바이트를 16진수와 ASCII로 출력함
- 주요 섹션은 다음과 같음
.text: 프로그램의 실제 코드, 즉 어셈블리가 들어 있으며 _start와 main이 포함됨
.rodata: 읽기 전용 데이터가 들어가며 예제에서는 "Penguin!" 문자열이 있음
.interp: 동적 링커 파일 이름이 들어 있음
- 섹션과 세그먼트는 사용 시점이 다름
- 섹션은 링크 시점에
ld가 사용함
- 세그먼트는 실행 시점에 사용됨
readelf --sections hello로 섹션의 메타데이터를 더 자세히 볼 수 있음
- 예제의 플래그를 보면 각 섹션의 성격이 드러남
.text: 실행 가능하고 읽기 전용
.rodata: 읽기 전용
.data: 읽기/쓰기 가능
.bss: 쓰기 가능한 데이터 영역
디스어셈블로 기계어를 어셈블리로 봄
.text 섹션에는 CPU가 코드로 해석해 실행하는 바이트들이 들어 있음
- 예제의
.text 시작 바이트 31 ed는 사람이 바로 의미를 알기 어렵기 때문에 디스어셈블러가 필요함
objdump -d ./hello는 .text 섹션을 디스어셈블해 어셈블리 명령어로 보여줌
- 예제 출력에서
31 ed는 xor %ebp,%ebp로 표시됨
- 이 방식으로 바이너리 안의 코드 바이트가 어떤 어셈블리 명령어인지 확인할 수 있음
세그먼트는 실행 시 메모리 배치를 정함
- 실행 파일은 세그먼트 또는 프로그램 헤더(program headers)로도 구성됨
readelf --segments hello는 프로그램의 세그먼트와 섹션-세그먼트 매핑을 보여줌
- 세그먼트는 프로그램의 각 부분을 메모리에 어떻게 나눠 배치할지 결정하는 데 쓰임
- 예제에는 두 개의 주요
LOAD 세그먼트가 있음
- 첫 번째
LOAD: R E로 표시되어 읽기/실행 가능함
- 두 번째
LOAD: RW로 표시되어 읽기/쓰기 가능함
.text는 읽고 실행해야 하지만 쓰면 안 되므로 첫 번째 세그먼트에 들어감
.data와 .bss는 쓸 수 있어야 하지만 실행할 필요가 없으므로 두 번째 세그먼트에 들어감
더 살펴볼 도구와 자료
- ELF 실행 파일은 특별한 마법이 아니라 일반적인 파일 형식이며, Linux 바이너리는
readelf, nm, objdump로 조사할 수 있음
- 관련 자료