ELF 파일의 동적 링크 과정에 대해 설명함
커널은 ELF의 PT_LOAD 세그먼트를 매핑하고, PT_INTERP로 지정된 동적 링커(ld.so) 를 로드한 뒤 제어를 넘김
이후 동적 링커가 스스로 재배치(relocation)하고 필요한 공유 객체를 mmap/mprotect로 로드함
이 구조는 스크립트의 shebang(#!) 메커니즘과 유사하다고 비유함
커널은 섹션 정보에는 전혀 관심이 없고, 오직 PT_LOAD 세그먼트만 처리함
예전에 objcopy로 ELF에 임의 파일을 삽입하려다 커널이 로드하지 않아 혼란스러웠던 경험을 공유함
결국 직접 프로그램 헤더 테이블 패치 도구를 만들었고, mold 링커에도 이 기능이 추가되었다고 함
관련 글: Self-contained Lone Lisp Applications
작성자가 이전에 내용을 잘못 수정해 올렸음을 인정하고 수정하겠다고 함
리눅스에서는 로더가 사용자 공간에서 동작하는데, 왜 더 다양한 로더가 없는지 항상 궁금했다고 함
읽어보니 의외로 단순하고 취약하지 않아서 흥미로웠다고 함
모든 함수를 main(100+n, ...) 형태로 바꾸면 된다고 농담함
이 주제에 흥미가 있다면 자신이 만든 cpu.land를 참고하라고 함
메모리 레이아웃보다는 멀티태스킹과 코드 로딩 과정을 다룸
cpu.land를 정말 좋아한다고 감사 인사를 전함
C 프로젝트 중 표준 라이브러리를 피하고 Linux syscall만 직접 호출하는 경우가 얼마나 될지 궁금하다고 함
이렇게 코드를 짜는 게 훨씬 재미있다고 느낌
직접 syscall을 쓰는 건 오히려 비효율적이라고 주장함
ALSA, DRM 같은 기능은 커널 syscall 대신 시스템 라이브러리를 통해 접근하는 게 이점이 많음
이 방식이 이식성과 유지보수성 면에서 Windows 스타일 접근보다 낫다고 설명함
Windows에서는 Win32 API만 사용하면 C 런타임을 링크하지 않아도 된다고 덧붙임
자신도 예전에 liblinux 프로젝트를 만들어 syscall만으로 프로그램을 작성했었다고 함
지금은 Linux의 nolibc 헤더가 잘 되어 있어 중단했지만,
현재는 syscall 기반의 Lisp 인터프리터 언어를 개발 중이라고 함
시스템 호출로 직접 Linux 유저 스페이스를 구성하는 실험이라 매우 흥미로운 여정이었다고 함
이식성을 유지하려 하지만, 파일 디스크립터는 너무 편리해서 포기하기 어렵다고 함
많은 드라이버 코드가 실제로 syscall만 사용한다고 덧붙임
ELF 인터프리터(ld.so)가 초기 ELF 세그먼트를 매핑한 후 모든 로딩을 담당한다고 설명함
execve는 PT_LOAD 세그먼트를 매핑하고 aux vector를 스택에 채운 뒤
ELF 인터프리터의 엔트리 포인트로 점프함
커널은 PLT/GOT에 대해 아무것도 모름
대학에서 이 주제를 가르치는 사람으로서, 학생들이 메모리 다이어그램 때문에 혼란스러워한다고 함
교재는 주소가 높을수록 위쪽에 그려지지만, 실제 Linux 프로세스는 낮은 주소가 위, 높은 주소가 아래로 출력됨 /proc/<pid>/maps를 보면 스크롤을 아래로 내릴수록 주소가 커짐
즉, “heap은 위로 자라고(stack은 아래로 자란다)”는 표현은 숫자상의 방향일 뿐,
시각적으로는 오히려 반대임
IDE처럼 아래로 갈수록 주소가 커지는 식으로 그리면 훨씬 직관적이라고 제안함
스택은 어쨌든 스택 포인터가 감소하면서 자라므로 “아래로 자란다”는 표현이 여전히 맞다고 함
다만 시각화는 가로 방향으로 하는 게 더 자연스럽다고 제안함
자신도 예전에 같은 혼란을 겪었고, 리틀엔디언 주소 표기가 헷갈렸다고 회상함
실제 사물의 쌓이는 방향을 생각하면 “스택이 아래로 자란다”는 표현이 직관적이지 않다고 반박함
오래된 PIC16 마이크로컨트롤러로 이런 실험을 하는 걸 좋아한다고 함
스택 포인터, 타이머, 변수 설정 등을 직접 다루는 게 재미있다고 느낌
shebang(#!) 관련 경험을 공유함
Java 애플리케이션이 실행 스크립트를 찾지 못한다는 오류를 냈는데,
실제 문제는 스크립트의 shebang 경로가 잘못된 것이었음
로컬에서는 잘 실행됐지만, 원격 서버의 인터프리터 경로가 달라서 생긴 문제였음
이건 Java만의 문제가 아니라, ENOENT 오류가 발생하는 모든 프로그램에서 생길 수 있다고 함 strace로 실행하면 어떤 syscall에서 오류가 났는지 바로 확인 가능하다고 조언함
Hacker News 의견
ELF 파일의 동적 링크 과정에 대해 설명함
커널은 ELF의 PT_LOAD 세그먼트를 매핑하고, PT_INTERP로 지정된 동적 링커(ld.so) 를 로드한 뒤 제어를 넘김
이후 동적 링커가 스스로 재배치(relocation)하고 필요한 공유 객체를 mmap/mprotect로 로드함
이 구조는 스크립트의 shebang(#!) 메커니즘과 유사하다고 비유함
예전에 objcopy로 ELF에 임의 파일을 삽입하려다 커널이 로드하지 않아 혼란스러웠던 경험을 공유함
결국 직접 프로그램 헤더 테이블 패치 도구를 만들었고, mold 링커에도 이 기능이 추가되었다고 함
관련 글: Self-contained Lone Lisp Applications
전체 코드를 main() 이전 혹은 main() 없이 패킹하는 실험을 했다고 함
관련 글: Packing a codebase into a single function
모든 함수를 main(100+n, ...) 형태로 바꾸면 된다고 농담함
이 주제에 흥미가 있다면 자신이 만든 cpu.land를 참고하라고 함
메모리 레이아웃보다는 멀티태스킹과 코드 로딩 과정을 다룸
C 프로젝트 중 표준 라이브러리를 피하고 Linux syscall만 직접 호출하는 경우가 얼마나 될지 궁금하다고 함
이렇게 코드를 짜는 게 훨씬 재미있다고 느낌
ALSA, DRM 같은 기능은 커널 syscall 대신 시스템 라이브러리를 통해 접근하는 게 이점이 많음
이 방식이 이식성과 유지보수성 면에서 Windows 스타일 접근보다 낫다고 설명함
지금은 Linux의 nolibc 헤더가 잘 되어 있어 중단했지만,
현재는 syscall 기반의 Lisp 인터프리터 언어를 개발 중이라고 함
시스템 호출로 직접 Linux 유저 스페이스를 구성하는 실험이라 매우 흥미로운 여정이었다고 함
ELF 인터프리터(ld.so)가 초기 ELF 세그먼트를 매핑한 후 모든 로딩을 담당한다고 설명함
execve는 PT_LOAD 세그먼트를 매핑하고 aux vector를 스택에 채운 뒤
ELF 인터프리터의 엔트리 포인트로 점프함
커널은 PLT/GOT에 대해 아무것도 모름
대학에서 이 주제를 가르치는 사람으로서, 학생들이 메모리 다이어그램 때문에 혼란스러워한다고 함
교재는 주소가 높을수록 위쪽에 그려지지만, 실제 Linux 프로세스는
낮은 주소가 위, 높은 주소가 아래로 출력됨
/proc/<pid>/maps를 보면 스크롤을 아래로 내릴수록 주소가 커짐즉, “heap은 위로 자라고(stack은 아래로 자란다)”는 표현은 숫자상의 방향일 뿐,
시각적으로는 오히려 반대임
IDE처럼 아래로 갈수록 주소가 커지는 식으로 그리면 훨씬 직관적이라고 제안함
다만 시각화는 가로 방향으로 하는 게 더 자연스럽다고 제안함
오래된 PIC16 마이크로컨트롤러로 이런 실험을 하는 걸 좋아한다고 함
스택 포인터, 타이머, 변수 설정 등을 직접 다루는 게 재미있다고 느낌
shebang(#!) 관련 경험을 공유함
Java 애플리케이션이 실행 스크립트를 찾지 못한다는 오류를 냈는데,
실제 문제는 스크립트의 shebang 경로가 잘못된 것이었음
로컬에서는 잘 실행됐지만, 원격 서버의 인터프리터 경로가 달라서 생긴 문제였음
strace로 실행하면 어떤 syscall에서 오류가 났는지 바로 확인 가능하다고 조언함
디버깅 중에 메인 바이너리의 재배치 순서가 언제 적용되는지 항상 헷갈린다고 함
링커가 자신의 심볼을 해결하기 전인지 후인지가 마치 블랙매직 같다고 표현함
마크다운 내 “lang_start function (defined here)” 부분의 링크가 깨져 있음을 지적함