- 프로그램 로드
- 섹션은 어떻게 실제 메모리에 매핑되는가?
- 페이징, 페이지드 세그멘테이션
ELF
Executable and Linkable Format
실행 가능하거나, 링크할 수 있는 파일들의 사전 정의된 구조(Format)
ex) .o, .so, .out(실행파일)
ELF 구조
프로그램은 메모리에 올라가기 전, 디스크에 있는 바이너리를 말함.
(참고) 섹션 vs 세그먼트
링킹하는 관점이냐?, 실행하는 관점이냐? 에 따라 이름이 다름.
→ 따라서 ‘readelf 를 한다’ → 단위는 ‘섹션’ →
readelf -S
프로세스 ELF 구조 (Segments)
- ELF 헤더
- 타입 - 얘가 EXEC(.out)인지, REL(.o, .a)인지, DYN(.so)인지…
- .text
- 사용자 작성 부분이 컴파일(.c→.o→asm→0101010)되어 담김
- .rodata
- 문자열 등 상수
- .data
- ‘초기화 된’ 전역 변수, 정적 변수
정적 변수가 왜 data에 담김?
static int a = 123;
은 .data에 담기는가?그렇다. 함수 호출을 반복해도 유지해야 하기 때문이다. (전역변수와 생명주기가 비슷)
- .bss (’Block Started by’ Symbol)
- ‘초기화 되지 않은’ 전역 변수, 정적 변수
.data와 .bss의 분리 기준
‘프로그램이 0으로 초기화를 하고 시작해야 하는가?’ 가 기준.
bss영역은 프로그램이 시작할 때 초기화하고 시작함.
- .symtab
- 심볼 테이블: 함수, 변수의 심볼을 심볼 이름(str)과 매핑할 수 있는 정보가 있음
- num: 일종의 id인듯?
- value: 실제 함수가 있는 주소
- .strtab
- 스트링 테이블: 함수 이름이 있는 테이블
0x1
부터 다음null
까지 → “hello.c\0” (+8 bytes)0x9
부터 다음null
까지 → “main\0” (+5 bytes)0xE
(014)부터 다음null
까지 → “puts\0” (+ 5bytes)- 제거(
strip
) - .symtab과 .strtab은 정적 링크가 완료된 이후에는 필요없는 내용.
- 참고
1, 9, e는 뭐예요?
총 18 bytes → 0x13 bytes
따라서 공유 라이브러리나 실행 파일에서는 직접 제거하곤 한다.
정의하기
정적 링킹 & 로딩이 무엇인가?
정적 링킹
= 심볼테이블 병합 및 재구성
링킹 시 참조 대상을 모두 resolve(symbol resolution) 후, 바이너리에 탑재.
정적 로딩
별거없고, 실행 시 메모리에 올림(로드)
동적 로딩 & 동적 링킹이 무엇인가?
정적 링킹 시 참조 대상의 링크 정보만을 탑재,
load-time에 필요한 라이브러리를 메모리에 로드 및 재배치,
실제 함수 호출시 다이나믹 링커가 라이브러리의 함수로 링크한다.
링크 및 로드 과정
정적 링킹 →
- (symbol resolution 과정을 통해) 바이너리에 함수 및 그 주소가 내장된다. (링킹)
- ex) 정적 링킹을 한 경우, 다른 목적 파일 의 함수 기계어는 .text에, 정적 변수는 .data/.bss에 이미 내장되어있다.
결과적으로 .text 부분에 해당 함수 주소로 바로 점프하도록 박혀있어 함수 호출에 별도 symbol resolution이 필요없다.
정적 로딩 →
- 로드 시 메모리에 그대로 적재한다. (일부 section 제외)
동적 로딩 →
*여기서부터는 동적 링킹을 사용한 경우에만 해당한다.(정적 링킹한다면, 이미 symbol resolution이 끝나있다)
- (참고) ‘동적 링킹을 사용할 때’의 정적 링킹
- 정적 링킹 시에 함수를 특정할 수 있는 정보만 삽입
- (참고)
동적 링킹이 필요한 경우 정적 링킹 과정에서 심볼들이 삽입된다.
→
.dynsym
, .dynstr
에 symbol resolution을 위한 정보 삽입부분 동적 링킹은 gcc의 기본 동작이다
gcc는 별도로 옵션 (
-static
)을 지정하지 않으면 동적 링킹 방식을 사용한다.그러나 이 때에도 지정한 목적 파일 (
gcc add.o sub.o
)는 정적 링킹된다.(공유 라이브러리
.so
가 아니므로, 동적 로드가 불가하기 때문이다)- 라이브러리 로드
- 커널이 .interp를 참고해 ld.so를 로드한다(프로세스 실행 과정의 일부)
- ld.so의
dl_start()
→dl_map_object()
n회 호출 (라이브러리 갯수만큼) - .dynamic 세그먼트를 참고해 .so 로드
참고
참고
- 재배치
- ld.so의
dl_relocate_object()
n회 호출 (라이브러리 갯수만큼)
세그먼트(로드된 섹션)에서 Read-only 세그먼트 할당
동적 링킹(lazy-binding)
- 함수 호출 시 → 정적 링킹에서 삽입된 정보를 통해 함수 호출로 lazy binding한다.
- 이어서 함수 호출이 일어남.
- 두번째 호출 부터는 바인딩이 필요 없음
정적 링킹을 명시한 경우 동적 링킹이 일어나지 않는다(
gcc -static
)이미 symbol resolution이 종료되었기 때문이다.
→ libc.so 등 외부 라이브러리도 이미 정적 링킹되었다. (자동임)
동적 링킹 과정(Detailed)
위의 내용 중 동적 로딩은 완료되었다고 보고, 그 이후 동적 링킹이 실제로 어떻게 수행되는지 살펴본다.
동적 링킹이 완료되지 않은 경우(첫 실행)
6/12 다시 간단 정리
함수 호출하면 먼저 .plt의 해당 함수 영역으로 감
해당 영역은 .got.plt를 참조해 점프하도록 함
그러나 첫 호출시 .got.plt에는 함수@.plt 아래의 [ reloc offset 푸시 + dl_runtime_resolve@.plt로 점프] 인스트럭션 부분 주소가 있음.
이걸 카운터 기준으로 정리하자면 함수@.plt에서 →(.got.plt 참조해서) 함수@.plt+6으로 갔다가 → .plt+0(dl_runtime_resolve@.plt)으로 갔다가 → (.got.plt 참조해서) 실제 라이브러리의 dl_runtime_resolve 호출함
그러면 dl_runtime_resolve는 .rel.plt 참고해서 .got.plt의 해당 함수 주소를 실제 라이브러리의 주소로 수정하겠죠?
그러면 마침내 링킹이 끝나고, dl_runtime_resolve에서 .got.plt의 바뀐 주소 참조해서 점프시킴
외부에서 보면 알아서 링킹하고 함수 실행까지 한 것으로 보이겠죠?
- (0)
.text
의 기계어 코드에 의해add@.plt
로 점프(call)
.text → .plt
- (1)
add@.got.plt
가 가리키는 곳으로 점프
.text → .plt — .got.plt 참조
(1-a)
add@.got.plt
참조add@.got.plt
을 봤더니 → add@.plt+6
의 주소(0x0804845e)가 있음아직 symbol resolution 이전이기 때문임.
동적 링킹이 끝난 상태라면, 라이브러리의 함수 주소가 있을것.
- (2)
add@.plt+6
로 점프
.text → .plt — .got.plt 참조 —> .plt
(1) → (1-a) 과정으로 프로그램 카운터는
add@.plt+6
로 이동됨- (3)
.plt+0
으로 점프
.text → .plt — .got.plt 참조 —> .plt → .plt
여기서부터 reloc 오프셋을 푸시하고,
reloc offset?
GOT 테이블에 할당된 주소를 라이브러리 함수 주소로 재할당
dl_fixup() 시
.rel.plt
참조용- (4)
0x8049ffc
가 가리키는 곳(.got.plt 범위)으로 점프
.text → .plt — .got.plt 참조 —> .plt → .plt → .got.plt
점프 전에 pushl 을 하는 것은 ‘링크맵 구조체 포인터’를 스택에 삽입하는 것(아직 몰라도 됨)
그 이후 0x8049ffc가 가리키는 곳(_dl_runtime_resolve()의 주소)으로 점프함
- (5) _dl_runtime_resolve() 점프
- (6) 끝났으니까 .got.plt의 바뀐 주소 참조해서 점프
이렇게 하는 ASM은 ld.so:_dl_runtime_resolve()에 있음.
개쩌는 레퍼런스
- 자세히 말고, 전체적으로 참고하면 좋을 듯.
- ㅇㅇ
Symbol Resolution — _dl_runtime_resolve()
이제 libcalc.so의 베이스에 더하면 → 함수 주소
.rel.plt를 참고해 .got.plt에 매핑해주면 됨.
.plt, .got, .got.plt 🆚 .dynsym, .dynstr
.plt, .got, .got.plt는 함수 호출 시 항상 참고함
반면 .dynsym, .dynstr은 ld.so의 _dl_runtime_resolve() 가 symbol resolution 과정에서 참고하는 용도 (동적 링킹시에만 쓰임)
그래서 .text 영역의 코드를 호출하면 → .plt로 갔다가 → 바로 .got.plt로 점프시키고, .got.plt
의문점(주제 외)
Base Address & Entry Point
- Base address 자체는 ‘시작 주소’라는 의미. 여기에 offset을 더해서 상대 주소를 Virtual Address로 바꿈.
- 프로세스의 base address는 이미지(바이너리) 베이스가 시작하는 주소
- 보통 0x1000부터 시작된다고 하는데, 그 이전은 보안을 위해 비워놓는다 함.
- Entry point는 프로그램 진입점
- main() 근처
동적 링킹에 재배치 과정이 포함되나?
아니다. 재배치 과정은 동적 로딩의 일부.
동적 로딩(+재배치) 이후 동적 링킹이 수행된다
PIC(아직 잘모르겠음)
상대주소로 되어있음.
.dynsym, .dynstr은 필수인가?
- 동적 링킹의 필수요소인가? 혹은 인간이 보기 위한 부가정보인가?(.symtab, .strtab 처럼?)
필수 요소이다.
정적 링킹을 한 경우, 이미 symbol resolution이 끝났으므로 필요없으나, 동적 링킹이 필요한 경우 필수.
- strip하면 사라지는가?
NO
- Reference
중간중간 참고할 자료
물리 및 가상 메모리 영역
- 물리 메모리가 1GB인데 어떻게 4GB를 사용중이죠?
- 스왑 메모리
프로세스 메모리 구조
- 커널 영역은 어디?
- 가상 메모리의 끝부분(0xC0000000)은 커널 영역으로 사용된다
- base address 이전은?
프로세스 생명주기
의문점
- 정적 링킹이랑 동적 링킹이 구분이 잘 안됨.
- 정적 로드 후, 일부를 동적 로드하는 것이다.
- 둘은 대비되는 개념이 아니라 기본이 정적 로드, 동적 로드는 필요한 경우 발생하는 추가적인 절차
Q. 정적 링킹 → 정적 로드 되어야 하는가?
A. 이건 모든 프로그램의 필수 조건
Q. 동적 링킹을 사용한 경우는?