근본 원인 :
- "컴파일 시점에 최종 메모리 주소를 모름"
이 하나의 문제에서 모든 개념이 파생
모르는 이유
이유 1. 여러 ".o 파일"이 합쳐져야 최종 배치가 결정
-> 내부 심볼 주소를 모름
-> ld(링커)가 릴로케이션을 하여 해결
이유 2. "공유 라이브러리"는 실행 시점에 메모리에 올라감
-> 외부 심볼 주소 모름
-> ld.so(동적 링커)가 해결
공통점 : 둘 다 "절대주소 안 쓰는 코드 생성"
차이점 : 내부 전역 심볼을 GOT로 가느냐 마느냐
PIE (실행 파일)
-> 내 전역변수는 나만 쓰니까 RIP-relative로 직접
-> 외부만 GOT/PLT
PIC (.so)
-> 내 전역변수도 다른 데서 덮어쓸 수 있으니까 GOT 경유
-> 외부도 당연히 GOT/PLT
왜?
-> symbol iterposition : .so의 전역 심볼은 실행 파일이나 다른 .so 같은 이름으로 재정의 가능
-> 그래서 "내 변수인데 최종 주소가 내 거 아닐 수 있음"
-> 그래서 안전하게 GOT를 거침
소스코드의 목적에 따른 컴파일
소스코드 → 실행파일 (.out, ELF executable)
"직접 실행되는 프로그램"
예: ./a.out, /usr/bin/ls
소스코드 → 공유 라이브러리 (.so)
"다른 프로그램이 가져다 쓰는 부품"
예: /lib/x86_64-linux-gnu/libc.so.6
---------------------------------------------------
libc.so를 만드는 소스코드 -> -fPIC로 컴파일
a.out을 만드는 소스코드 -> -fPIE로 컴파일
같은 C 소스코드라도 "최종 산출물이 뭐냐"에 따라 컴파일 옵션이 달라짐
GOT = 테이블 (데이터)
-> 주소가 저장되는 "칸"
-> 변수든 함수든 실제 주소가 여기 들어감
-> 두 종류 :
.got = 변수용 (eager, 로드 시 바로 채움)
.got.plt = 함수용 (lazy, 첫 호출 시 채움)
PLT = 코드
-> 외부 "함수" 호출 전용 트램펄린
-> GOT를 읽어서 점프하는 짧은 코드
-> 변수 접근에는 PLT가 안 쓰임
즉 :
외부 변수 접근 : 코드 -> GOT -> 실제 변수
외부 함수 호출 : 코드 -> PLT -> GOT -> 실제 함수
PLT가 함수에만 있는 이유 :
-> 함수는 lazy binding이 가능 (첫 호출까지 resolve 지연)
-> 변수는 접근하는 순간 바로 값이 필요하니까 lazy 불가
Lazy Binding
프로그램이 시작할 때 외부 함수 주소를 전부 찾아두는 게 아니라, 실제로 호출되는 순간에 비로소 주소를 찾는 방식