리눅스 커널의 이해
12

ch20. exec

linuxfilesystemkernel
EXEC · CH.20
LECTURE NOTE — LINUX KERNEL, CHAPTER 20

프로그램 실행, 프로세스에
새 세계가 열리는 순간

exec는 단순히 새로운 프로세스를 생성하는 함수가 아닙니다. 기존 프로세스의 껍데기는 유지한 채, 그 내부의 영혼을 통째로 갈아 끼우는 강력한 작업이죠. 터미널에서 ls 명령어 하나를 치는 순간, 파일시스템부터 권한 모델, 메모리 관리, 동적 링크까지 시스템의 모든 요소가 유기적으로 맞물려 돌아가기 시작합니다.

shell: 자식 프로세스 생성 커널: 문맥 교체 중 dynamic linker → main()
01

큰 그림: 프로그램은 파일이고, 프로세스는 실행 중인 세계다PROGRAM vs PROCESS

터미널에 ls라고 치는 순간, 컴퓨터 안에서는 실제로 무슨 일이 벌어질까요? 겉보기엔 아주 단순합니다. 명령어 하나를 입력했을 뿐인데 금세 파일 목록이 쏟아져 나오니까요. 하지만 커널의 관점에서 보면 이는 꽤 거대한 스케일의 이벤트입니다.

프로그램은 디스크에 고이 잠들어 있는 정적인 실행 파일(예: /bin/ls)입니다. 반면 프로세스는 그 프로그램이 메모리에 적재되어 살아 숨 쉬는 동적인 상태를 의미합니다. 이번 장의 핵심 주제는 바로 디스크 속 실행 파일이 어떻게 커널에 의해 온전한 '실행 문맥(Execution Context)'을 갖춘 새로운 프로세스로 깨어나는가에 대한 이야기입니다.

프로그램이 종이에 적힌 "요리 레시피"라면, 프로세스는 그 레시피를 바탕으로 실제 주방에서 셰프가 불을 켜고 재료를 다듬고 있는 "요리 현장"입니다. 레시피만 있다고 요리가 완성되지는 않죠. 주방, 재료, 조리도구, 셰프의 현재 동작까지 모든 것이 필요합니다. 이 전체 상태를 우리는 실행 문맥이라고 부릅니다.

이번 장에서는 크게 여섯 가지 파트를 다룹니다. 커널이 실행 문맥을 어떻게 탈바꿈시키는지, 프로세스의 보안 권한(UID·EUID·setuid·capability), 명령행 인자와 환경 변수가 전달되는 방식, 복잡한 메모리 영역의 배치(text·data·bss·heap·stack), 디버거가 남의 프로세스를 엿보는 원리(ptrace), 그리고 대망의 ELF 파일 형식과 execve() 시스템 콜의 내부 동작까지 살펴볼 것입니다.

명심하세요. exec는 새 프로세스를 만드는 함수가 아닙니다. 기존 프로세스의 PID와 같은 껍데기는 그대로 둔 채, 내부의 실행 내용만 새로운 것으로 덮어씌웁니다. fork()가 도화지를 복사하는 것이라면, execve()는 그 도화지 위에 완전히 새로운 그림을 그리는 것입니다.

무대가 있고 배우가 있습니다. 여기서 프로세스는 무대이고 프로그램은 대본입니다. execve()는 한창 진행 중이던 연극을 멈추고, 같은 무대 위에서 완전히 다른 세트장과 대본으로 새로운 연극을 올리는 작업입니다. 관객 입장에서는 전혀 다른 극이 시작된 것 같지만, 극장의 무대 번호(PID)는 변함이 없습니다.

핵심 O/X 퀴즈

1. 프로그램은 디스크에 저장된 실행 파일이고, 프로세스는 실행 중인 계산 상태를 포함한다.
O프로그램은 정적 파일, 프로세스는 동적 실행 상태입니다.
2. execve()는 항상 새로운 PID를 가진 새 프로세스를 만든다.
XPID는 유지되고, 프로세스 안의 실행 내용만 교체됩니다.
3. 오늘 주제의 핵심은 실행 파일 정보를 바탕으로 커널이 프로세스 실행 문맥을 새로 구성하는 과정이다.
O실행 문맥 교체가 이번 장의 중심 주제입니다.
02

실행 파일과 execve: 되돌아오지 않는 여행FORK + EXEC PATTERN

사용자가 ls를 입력하면, 셸은 PATH 환경 변수를 뒤져 /bin/ls라는 파일을 찾아냅니다. 그런 다음 셸은 보통 fork()를 호출해 자신과 똑같이 생긴 자식 프로세스를 하나 낳습니다. 여기까지 자식은 부모인 셸과 완벽히 동일한 상태입니다.

이후 자식 프로세스가 execve("/bin/ls", argv, envp)를 호출하면, 커널의 sys_execve()가 개입합니다. 커널은 해당 경로의 파일이 진짜 존재하는지, 실행할 권한은 있는지, 어떤 형태의 실행 파일(ELF인지, 쉘 스크립트인지)인지 꼼꼼히 확인한 후, 기존 셸의 문맥을 밀어버리고 /bin/ls의 세계로 문맥을 재구성합니다.

1
파일 검사. 대상 파일이 ELF 바이너리인지, 스크립트인지 등 포맷을 판별합니다.
2
문맥 교체. 예전 셸의 코드와 데이터는 메모리에서 날아가고, /bin/ls의 코드·데이터·스택이 새롭게 자리를 잡습니다.
3
파일 디스크립터 상속. 부모로부터 물려받아 이미 열려 있던 파일 디스크립터들은 기본적으로 유지됩니다. (단, close-on-exec 플래그가 붙어있다면 깔끔하게 닫힙니다.)
4
새로운 시작. 시스템 콜이 성공적으로 끝나면, 예전 코드로 복귀하는 것이 아니라 새 프로그램의 진입점(Entry Point)부터 코드가 실행됩니다.

ls > out.txt를 실행하는 상황을 상상해 보세요. 자식 프로세스는 스스로의 표준 출력을 파일로 향하게 꺾어둔 뒤, execve()/bin/ls를 실행합니다. 새로 깨어난 /bin/ls는 자기가 출력을 파일로 보내고 있다는 사실조차 모르지만, 이미 표준 출력이 파일로 연결되어 있기 때문에 결과가 알아서 파일에 기록됩니다. 이처럼 '열린 파일 디스크립터가 exec 이후에도 살아남는 특성'이 바로 유닉스의 강력한 파이프와 리다이렉션을 가능하게 합니다.

성공적으로 수행된 exec는 절대 원래의 호출 지점으로 돌아오지 않습니다. 만약 돌아왔다면? 그건 100% 실행에 실패했다는 뜻입니다. 성공했다면 돌아갈 기존 코드 자체가 이미 메모리에서 삭제되었기 때문이죠.

핵심 O/X 퀴즈

1. 셸이 외부 명령을 실행할 때는 보통 자식 프로세스를 만든 뒤 그 자식이 execve()를 호출한다.
Ofork 후 exec 패턴이 유닉스의 근간입니다.
2. execve()가 성공하면 기존 프로그램의 호출 지점으로 돌아와 다음 줄을 실행한다.
X성공하면 이전 코드 자체가 사라지므로 영원히 돌아오지 않습니다.
3. close-on-exec 플래그가 없는 열린 파일 디스크립터는 새 프로그램 실행 후에도 유지될 수 있다.
O이 성질 덕분에 리다이렉션과 파이프 구현이 가능합니다.
03

프로세스 권한: UID, EUID, setuid의 의미CREDENTIALS

멀티유저 시스템에서 가장 민감한 질문은 "이 프로세스가 과연 이 행동을 할 자격이 있는가?"입니다. 리눅스는 프로세스마다 꼬리표처럼 credentials(권한 정보)를 달아둡니다. 파일 시스템의 VFS는 파일의 소유자/권한 비트와 프로세스의 이 꼬리표를 대조하여 접근 허용 여부를 결정합니다.

Table 20-1 — 프로세스 credentials 주요 필드
uid / gid
실제(Real) 사용자 및 그룹 ID. 이 프로세스를 처음 실행한 '진짜 주인'이 누구인지 나타냅니다.
euid / egid
유효(Effective) 사용자 및 그룹 ID. 대부분의 권한 검사에서 "지금 당장 누구의 권한으로 행동하고 있는가"를 결정하는 핵심 ID입니다.
fsuid / fsgid
파일 시스템 접근 전용 유효 ID입니다.
suid / sgid
저장된(Saved) ID. setuid 프로그램이 필요에 따라 권한을 얻었다 버렸다 할 수 있도록 백업해 두는 용도입니다.
groups
사용자가 속한 보조 그룹들의 목록입니다.

여기서 중요한 것은 'Real ID'와 'Effective ID'의 차이입니다. 대표적인 예가 바로 /usr/bin/passwd입니다. 일반 사용자는 /etc/shadow 같은 중요한 비밀번호 파일을 함부로 수정할 수 없습니다. 그런데 passwd 실행 파일에는 특별한 'setuid' 비트가 켜져 있고 소유자는 root로 되어 있습니다. 따라서 일반 사용자가 이 프로그램을 실행하는 순간, 프로세스의 euid가 일시적으로 파일 소유자인 root로 승격됩니다. 덕분에 자신의 비밀번호를 안전하게 변경할 수 있는 것이죠.

평소에는 회사에서 일반 사원증을 매고 다닙니다. 그런데 서버실에 잠시 들어가야 할 일이 생기면 서명 후 '임시 마스터 키'를 넘겨받습니다. 작업이 끝나면 즉시 반납해야 하죠. setuid가 바로 이 임시 마스터 키입니다. 다만 키를 들고 있는 동안 사고를 치거나 해킹을 당하면 치명타를 입을 수 있으므로, setuid는 꼭 필요한 순간에만 얻고 곧바로 내려놓는 것이 원칙입니다. 이때 잠시 원래 신분증을 보관해 두는 곳이 바로 suid(Saved UID)입니다.

핵심 O/X 퀴즈

1. euid는 권한 검사에서 프로세스가 현재 어떤 사용자처럼 행동하는지를 나타내는 데 중요하다.
O대부분의 권한 검사는 이 euid를 1순위로 바라봅니다.
2. setuid 프로그램은 항상 안전하므로 특별한 주의가 필요 없다.
X버그나 취약점과 결합되면 즉각적인 시스템 탈취(root 획득)로 이어질 수 있는 가장 위험한 기능 중 하나입니다.
3. /usr/bin/passwd 같은 프로그램은 일반 사용자가 보호된 파일을 직접 편집하지 않고도 자기 비밀번호를 바꿀 수 있게 한다.
Osetuid 비트를 통한 일시적 권한 상승이 이를 가능하게 합니다.
04

권한을 더 잘게 쪼개기: capabilitiesTable 20-3

전통적인 유닉스 권한 모델은 'root인가, 아닌가'의 흑백논리로 아주 단순합니다. 하지만 이 모델은 너무 투박하죠. 어떤 웹 서버 프로그램이 80번 포트를 열기 위해 권한이 필요할 뿐인데, 전통적 모델에서는 어쩔 수 없이 무소불위의 root 권한을 쥐여주어야 합니다. 만약 이 프로그램이 해킹당하면 시스템 전체가 넘어가게 됩니다. 이를 막기 위해 도입된 정교한 개념이 바로 capability입니다.

Table 20-3 — 주요 capability 예시
CAP_CHOWN

파일 소유자나 그룹을 마음대로 변경할 수 있는 권한입니다.

CAP_DAC_OVERRIDE

일반적인 파일 읽기/쓰기/실행 접근 제어(DAC)를 무시하고 뚫고 들어갈 수 있습니다.

CAP_NET_ADMIN

인터페이스 설정 등 전반적인 네트워크 관리 권한입니다.

CAP_NET_BIND_SERVICE

1024번 미만의 잘 알려진 특권 포트(Privileged Port)에 바인딩할 수 있는 권한입니다.

CAP_SYS_TIME

시스템 시계를 변경할 수 있는 권한입니다.

CAP_SYS_PTRACE

다른 프로세스를 디버깅하거나 추적(ptrace)할 수 있는 막강한 권한입니다.

CAP_SYS_NICE

프로세스의 스케줄링 우선순위를 임의로 조작할 수 있습니다.

핵심은 간단합니다. "왕(root)의 왕관을 수십 개의 작은 보석(열쇠)으로 쪼개자"는 것입니다. 프로그램에 딱 필요한 보석 하나만 쥐여주면, 버그가 터지더라도 그 피해 범위가 해당 열쇠의 권한 밖으로 벗어나지 못합니다. 커널은 특정 특권 작업 직전에 capable() 함수를 호출해 프로세스가 해당 열쇠를 쥐고 있는지 검사합니다.

setuid가 "잠시 보안팀장 명찰 패용하기"라면, capability는 "딱 서버실 A구역 문만 열리는 카드키 발급받기"입니다. 시스템의 피해를 최소화하려는 최소 권한의 원칙(Principle of Least Privilege)이 커널 레벨에서 구현된 우아한 방식입니다.

핵심 O/X 퀴즈

1. capability는 root 권한을 여러 특권 작업 단위로 나누어 관리하려는 모델이다.
O권한의 세분화를 통해 보안 사고 시의 피해 반경을 최소화합니다.
2. CAP_SYS_TIME만 가진 프로세스는 원칙적으로 모든 root 권한을 가진 것과 같다.
X오직 시스템 시간 관련 작업만 허용됩니다. 포트를 열거나 파일을 지우려면 다른 capability가 필요합니다.
3. CAP_SYS_NICE는 프로세스 우선순위 조작 같은 작업과 관련된 capability이다.
Onice() 등 스케줄링 조작 권한 검사에 사용됩니다.
05

Linux Security Modules: 보안 판단을 끼워 넣는 자리LSM / SECURITY HOOKS

리눅스 커널 2.6부터 도입된 Linux Security Modules(LSM)는 커널의 보안 아키텍처를 완전히 뒤바꿔 놓았습니다. LSM의 핵심은 커널 내부의 중요한 길목마다 '보안 정책을 판단할 수 있는 톨게이트(Hook)'를 마련해 두고, 실제 어떤 잣대로 검사할지는 외부 모듈(SELinux, AppArmor 등)이 교체할 수 있도록 플러그인 형태로 만든 것입니다.

여기서 핵심 역할을 하는 것이 바로 security hook입니다. 파일에 접근하려 하거나, 네트워크 소켓을 열거나, 시스템 시간을 변경하려는 아슬아슬한 순간, 커널은 동작을 잠시 멈추고 해당 훅을 호출하여 "이 작업을 허용해도 좋습니까?"라고 보안 모듈에 묻습니다.

LSM 구조 핵심 요소
security_operations
각종 hook 함수들의 포인터가 모여 있는 묶음(구조체)입니다. 적용된 보안 모델의 실체입니다.
security_ops
현재 커널에서 활성화된 보안 모델의 hook 테이블 주소를 가리키는 전역 포인터입니다.
dummy_security_ops
별도의 보안 모듈이 없을 때 동작하는 기본 최소 보안 모델. 주로 앞서 배운 capability만을 확인합니다.
capable()
LSM 내부에서도 여전히 프로세스가 필요한 capability를 갖추었는지 확인하는 데 요긴하게 쓰입니다.

예를 들어 stime() 시스템 콜이 실행되면 내부적으로 settime security hook이 불립니다. 기본 모델(dummy)이라면 프로세스의 CAP_SYS_TIME 소유 여부만 묻고 통과시킵니다. 하지만 SELinux 같은 강력한 모듈이 로드되어 있다면, 해당 프로세스의 도메인과 대상 객체의 타입까지 깐깐하게 대조하여 허가증을 발급합니다.

거대한 사옥의 복도, 엘리베이터, 서버실 문앞마다 '경비원 호출 벨(Hook)'이 달려 있습니다. 이 벨을 눌렀을 때 어떤 경비업체가 전화를 받을지는 사장님 마음대로(플러그인) 바꿀 수 있습니다. 기본 경비업체는 "사원증만 있으면 통과" 수준이지만, 일급 비밀 보안업체(SELinux)로 교체하면 "사원증, 소속 부서, 시간대, 사전 결재 내역"까지 까다롭게 심사합니다. 커널은 문을 부수고 고칠 필요 없이, 계약할 경비업체만 갈아 끼우면 되는 셈이죠.

핵심 O/X 퀴즈

1. security hook은 커널이 보안 관련 작업을 수행하기 전에 호출할 수 있는 검사 함수다.
O커널 내부의 핵심 작업 직전에 보안 판단을 끼워 넣는 톨게이트입니다.
2. LSM은 리눅스에서 실행 파일 형식만 인식하기 위한 구조다.
XLSM은 보안 정책을 교체 가능하게 만들어주는 범용 접근 제어 프레임워크입니다.
3. 기본 보안 hook은 특정 작업에 필요한 capability를 검사하는 방식으로 동작할 수 있다.
O특별한 보안 모듈이 없다면 dummy_security_ops가 capability 기반의 통제만 수행합니다.
06

명령행 인자와 환경 변수: 새 프로그램에게 넘겨지는 말들Figure 20-1

ls -l /usr/bin을 실행할 때, 주인공인 /bin/ls에게 -l/usr/bin이라는 지시사항을 어떻게 전달할까요? 우리가 작성하는 C 프로그램의 main(int argc, char *argv[])가 바로 이 지시사항을 건네받는 창구입니다. 위 명령에서 argc는 3, argv[0]"ls", argv[1]"-l", argv[2]"/usr/bin"이 되며, 배열의 끝은 깔끔하게 NULL 포인터로 마감됩니다.

프로그램이 구동되는 백그라운드 환경 변수 역시 main(int argc, char *argv[], char *envp[])를 통해 전달됩니다. envpHOME=/home/userPATH=/usr/bin:/bin 같은 KEY=VALUE 형태의 문자열 포인터들을 담고 있는 배열이며, 이 역시 끝은 NULL로 장식됩니다.

Figure 20-1 — 사용자 모드 스택 하단 배치
argc
전달된 명령행 인자의 총 개수.
argv[]
명령행 인자 문자열들을 가리키는 포인터 배열. 배열의 끝은 NULL.
envp[]
환경 변수 문자열들을 가리키는 포인터 배열. 배열의 끝은 NULL.
동적 링커 보조 벡터
동적 링커(dynamic linker)가 초기화를 수행할 때 참고하는 보조 정보(auxiliary vector).
실제 문자열 데이터
포인터들이 가리키는 원본 문자열 데이터들이 스택의 가장 높은 주소에 밀집해 있습니다.

커널의 메모리 디스크립터(mm_struct)는 arg_start, env_start 등의 필드를 통해 이 문자열들이 메모리 어디에 박혀 있는지 정확히 추적합니다. x86 아키텍처 기준으로 이 데이터들은 사용자 모드 스택의 가장 높은 주소 영역에 자리 잡으며, 스택 자체는 그 아래를 향해(낮은 주소 쪽으로) 자라나게 됩니다.

execve()가 새 배우를 무대 위로 밀어 올리기 직전, 대기실 화장대 위에 놓아두는 쪽지와 같습니다. "오늘 넌 ls 역을 맡았고, 옵션은 -l, 타겟은 /usr/bin이야. 무대 환경(HOME, PATH)은 대략 이러해." 막이 오르고 프로그램이 시작되자마자, 배우는 이 쪽지(스택의 인자들)를 가장 먼저 집어 들고 자신의 역할을 파악합니다.

핵심 O/X 퀴즈

1. ls -l /usr/bin에서 -l/usr/bin은 명령행 인자로 전달될 수 있다.
O정확히 argv[1]과 argv[2]로 파싱되어 전달됩니다.
2. C 프로그램에서 argvenvp 배열은 일반적으로 NULL 포인터로 끝난다.
O배열의 끝을 명확히 알리기 위한 유닉스의 오랜 관례입니다.
3. 환경 변수는 커널 내부에만 저장되며 사용자 모드 스택에는 배치되지 않는다.
X프로그램이 직접 읽을 수 있도록 사용자 모드 스택 영역 최상단에 고이 모셔둡니다.
07

라이브러리: 정적 링크와 동적 링크STATIC vs DYNAMIC LINKING

단순히 return 0;만 수행하는 빈 C 프로그램조차도 실행을 준비하는 시작 코드(startup code)와 종료를 수습하는 뒷정리 코드가 반드시 필요합니다. 즉, 우리가 짠 핵심 로직 외에도 프로그램을 굴러가게 만드는 무수히 많은 '접착제 코드'와 라이브러리가 뒤에 숨어 있습니다.

컴파일러가 만들어낸 오브젝트 파일(.o) 안에는 기계어가 들어있지만, printf처럼 외부 라이브러리에 있는 함수의 실제 메모리 주소는 아직 텅 비어 있습니다. 흩어진 조각들을 하나로 끌어모아 이 구멍 난 주소들을 꿰매주는 녀석이 바로 링커(Linker)입니다.

정적 링크 vs 동적 링크 비교
정적 링크 static linking

사용하는 라이브러리의 코드를 통째로 복사해서 내 실행 파일 안에 욱여넣습니다. 외부 의존성 없이 독립적으로 실행되지만, 파일 크기가 비대해지고 메모리가 낭비됩니다. GCC의 -static 옵션.

동적 링크 dynamic linking

실행 파일에는 "나중에 libc.soprintf를 쓸 거야"라는 메모표만 남깁니다. 프로그램이 실행되는 순간 동적 링커(ld.so)가 개입해 메모리에 라이브러리를 올리고 주소를 연결합니다.

현대 OS가 사랑하는 동적 링크(공유 라이브러리)의 궁극적인 무기는 압도적인 메모리 절약입니다. 메모리 매핑 기술을 활용하면 libc.so를 100개의 프로세스가 쓰더라도 물리 메모리에는 단 한 벌의 읽기 전용 텍스트(코드) 영역만 올려두고 모두가 이를 공유하게 만들 수 있습니다.

정적 링크는 캠핑을 갈 때 텐트, 버너, 코펠, 식재료를 전부 내 배낭에 싸 짊어지고 가는 것입니다. 무겁지만 산골짜기 어디서든 혼자 살아남죠. 동적 링크는 텐트만 챙기고 "버너와 코펠은 캠핑장 대여소에서 빌려 쓸게"라는 메모만 적어가는 것입니다. 가벼워서 좋지만, 캠핑장(OS 환경)에 대여소(공유 라이브러리)가 없으면 아무것도 할 수 없습니다.

핵심 O/X 퀴즈

1. 오브젝트 파일은 외부 심볼 주소가 아직 해결되지 않았을 수 있으므로 그대로 실행할 수 없다.
O링커가 이 빈 공간(주소)들을 해결하고 나서야 비로소 실행 가능한 형태가 됩니다.
2. 공유 라이브러리는 항상 실행 파일 안에 라이브러리 코드를 전부 복사한다.
X파일 내부로 복사하지 않고, 실행 시점에 동적 링커가 메모리 맵핑으로 연결(공유)해 줍니다.
3. 동적 링커는 실행 시점에 필요한 공유 라이브러리를 찾아 프로세스 주소 공간에 매핑하는 역할을 한다.
O보통 리눅스에서는 ld.so가 이 골치 아픈 런타임 연결 작업을 도맡아 처리합니다.
08

프로그램 세그먼트와 프로세스 메모리 영역TEXT / DATA / BSS / HEAP / STACK

전통적으로 유닉스 프로그래머들은 프로세스의 가상 메모리 공간을 몇 개의 깔끔한 세그먼트(Segment)로 나누어 머릿속에 그립니다.

프로그램 세그먼트 구성
text segment
CPU가 긁어모아 실행할 기계어 코드 덩어리입니다. 보안과 공유를 위해 읽기/실행 권한만 부여되며 쓰기는 철저히 금지됩니다.
initialized data
코드에 int x = 10; 처럼 명시적인 초기값이 박혀 있는 전역/정적 변수들이 자리합니다.
bss (uninitialized data)
초기값이 없는 전역 변수들(예: int y;)을 위한 공간. 디스크 공간 낭비를 막기 위해 실행 파일에는 '크기' 정보만 달랑 남기고, 실제 실행될 때 커널이 0으로 가득 채워 메모리를 내어줍니다.
heap
malloc() 등에 의해 동적으로 할당되는 메모리 운동장. start_brk에서 시작해 높은 주소를 향해(brk) 점점 팽창합니다.
stack
함수 호출 시 반환 주소, 매개변수, 지역 변수가 임시로 쌓이는 공간. 높은 주소에서 시작해 낮은 주소 방향으로 파고들어 갑니다.

하지만 현대 운영체제의 주소 공간을 단순히 text, data, bss, stack이라는 네 가지 구역으로만 이해하기에는 부족합니다. mmap을 통한 파일 메모리 매핑, 익명 매핑, 수많은 공유 라이브러리들이 뒤섞이면서, 프로세스의 주소 공간은 거대한 연속된 덩어리가 아니라 파편화된 수많은 메모리 영역(Memory Region)들의 모자이크가 되었습니다. 내 프로그램의 text/data뿐 아니라, libc.so의 text/data, 동적 링커의 text/data, 심지어 커널과 통신하는 vsyscall 페이지까지 복잡하게 공존합니다.

과거의 집 구조는 "안방, 거실, 부엌, 화장실"의 4구역이면 설명이 끝났습니다. 하지만 현대의 최첨단 복합 빌딩은 공용 라운지, 임대용 오피스 구역, 지하 기계실, 보안 구역 등 다채로운 공간들로 쪼개져 있습니다. 큰 틀의 분류는 여전히 유효하지만, 실제 문을 열고 들어가 보면 훨씬 복잡하고 정교한 공간 분할이 이루어져 있는 셈입니다.

핵심 O/X 퀴즈

1. text segment에는 일반적으로 프로그램의 실행 코드가 들어간다.
O쓰기 권한이 없는 순수한 기계어 명령어들의 집합소입니다.
2. bss segment는 초기값이 실행 파일에 명시적으로 저장된 전역 변수만 담는다.
X정반대입니다. 초기값이 없는 전역 변수들이며, 디스크에는 자리만 맡아두고 메모리 적재 시 0으로 채워집니다.
3. 공유 라이브러리와 메모리 매핑 때문에 현대 프로세스 주소 공간은 단순한 네 개 세그먼트만으로 설명하기 어렵다.
O무수히 많은 VMA(Virtual Memory Area)들의 조합으로 이루어집니다.
09

메모리 배치: classical layout과 flexible layoutTable 20-4 / /proc/self/maps

32비트 x86 아키텍처에서 사용자 공간으로 3GB가 주어졌다고 칩시다. 전형적인 ELF 바이너리의 text 구역은 보통 0x08048000이라는 꽤 낮은 주소에서 출발합니다. 그 뒤를 data, bss, heap이 차례로 따르며 위로 자라나고, 반대편 천장인 0xc0000000 근방에서는 사용자 스택이 바닥을 향해 거꾸로 자라 내려옵니다.

두 가지 메모리 배치 방식 비교
classical layout

과거의 전통적인 방식. mmap으로 매핑되는 영역들이 주소 공간의 1/3 지점(대략 0x40000000)에 닻을 내리고 위로 자라납니다. 이 때문에 아래에서 올라오는 힙(Heap)이 자칫 1GB 한계에 부딪혀 성장이 막혀버리는 단점이 있었습니다.

flexible layout

커널 2.6.9부터 도입된 유연한 구조. mmap 매핑 영역을 스택 바로 아래쪽으로 끌어올려, 이번엔 밑을 향해 거꾸로 자라게 만들었습니다. 덕분에 아래에서 솟아오르는 힙이 쓸 수 있는 광활한 빈 공간이 확보됩니다.

이 영리한 flexible layout을 사용하려면 운영체제가 스택이 최대 어디까지 내려올지(RLIMIT_STACK) 미리 알고 있어야 안전거리를 확보할 수 있습니다. 내가 짠 프로그램이 메모리를 어떻게 썰어서 쓰고 있는지 궁금하다면 터미널에서 cat /proc/self/maps를 쳐보세요. 시작 주소, 끝 주소, rwx 권한, 매핑된 파일 이름까지 적나라한 주소 공간의 민낯을 구경할 수 있습니다.

즉, 프로그램을 실행한다는 것은 단순히 파일 내용을 메모리에 욱여넣는 작업이 아닙니다. 철저한 계산과 보안 규칙 아래, 수기가와 기가바이트 단위의 주소 공간을 정교하게 설계하고 배치하는 고도의 건축 작업입니다.

핵심 O/X 퀴즈

1. 80x86 기준 ELF 프로그램의 text segment는 보통 0x08048000에서 시작한다고 설명된다.
O과거 32비트 리눅스의 상징과도 같은 시작 주소입니다.
2. flexible layout은 heap과 mapping 영역의 활용 가능성을 줄이기 위해 도입되었다.
X반대입니다. 힙이 성장할 수 있는 공간을 극대화하기 위해 매핑 영역의 방향을 뒤집은 혁신이었습니다.
3. /proc/self/maps는 프로세스의 메모리 영역 배치를 관찰하는 데 사용할 수 있다.
O리눅스 시스템 프로그래머의 영원한 친구이자 최고의 디버깅 도구입니다.
10

실행 추적: 디버거는 어떻게 남의 프로세스를 들여다보는가ptrace() / Table 20-5

우리가 자주 사용하는 gdb 같은 디버거가 코드를 한 줄씩 멈춰가며 실행하고, 실시간으로 변수를 훔쳐보고 조작할 수 있는 마법의 근원은 무엇일까요? 바로 커널이 제공하는 ptrace()라는 무시무시한 시스템 콜 덕분입니다.

ptrace()는 말 그대로 한 프로세스(디버거)가 다른 프로세스(타겟)의 멱살을 잡고 실행 흐름을 완벽히 통제할 수 있게 해주는 기능입니다. 너무 강력하기 때문에 원칙적으로 나와 소유자가 같은 프로세스만 엿볼 수 있으며, 남의 프로세스를 디버깅하려면 CAP_SYS_PTRACE라는 특수한 권한이 필요합니다. 또한 한 프로세스는 오직 한 명의 추적자에게만 지배당할 수 있습니다.

Table 20-5 — 주요 ptrace 명령 (80x86)
PTRACE_ATTACH
지정한 대상 프로세스에 갈고리를 걸어 추적을 시작합니다.
PTRACE_DETACH
볼일을 다 본 후 갈고리를 풀고 원래 상태로 놔줍니다.
PTRACE_CONT
멈춰있는 프로세스에게 "다시 실행해"라고 명령을 내립니다.
PTRACE_SINGLESTEP
딱 어셈블리 명령어 한 줄만 실행하고 다시 멈추게 합니다 (x86의 TF 레지스터 트릭 사용).
PTRACE_SYSCALL
다음 시스템 콜을 호출하거나 빠져나올 때까지만 실행시키고 멈춥니다.
PTRACE_PEEKTEXT/DATA
메모리 공간을 뒤져 특정 주소의 값을 슬쩍 읽어옵니다.
PTRACE_POKETEXT/DATA
메모리 공간의 특정 주소에 내가 원하는 값을 강제로 덮어씁니다 (BreakPoint 삽입 등에 쓰임).

ptrace()로 묶이는 순간, 타겟 프로세스의 가짜 부모(parent)가 디버거로 바뀝니다. 명령어 한 줄이 끝날 때마다, 혹은 시스템 콜이 불릴 때마다 타겟은 얼음(멈춤) 상태가 되고 가짜 부모인 디버거에게 SIGCHLD 시그널이 날아가 상황을 보고합니다.

일반 실행이 배우가 무대 위에서 대본대로 쭉 연기하는 것이라면, 디버깅은 깐깐한 영화 감독이 메가폰을 잡은 촬영장입니다. 감독(디버거)이 "컷! 잠시 정지. 대본(메모리) 좀 다시 확인하자. 좋아, 다음 대사 한 마디만 해 봐(싱글스텝). 자 다시 액션!" 하고 지시하는 것과 완벽히 동일합니다. ptrace()는 커널이 이 감독에게 쥐여준 메가폰입니다.

핵심 O/X 퀴즈

1. ptrace()는 디버거가 다른 프로세스의 실행을 감시하고 제어하는 데 사용될 수 있다.
Ogdb, strace 같은 도구들을 뒷받침하는 핵심 커널 메커니즘입니다.
2. 하나의 프로세스는 동시에 여러 추적 프로세스에게 추적될 수 있다.
X사공이 많으면 배가 산으로 가듯, 동시에 두 명 이상의 추적자에게 조종당할 수는 없습니다.
3. PTRACE_SINGLESTEP은 한 개의 어셈블리 명령어 실행 후 다시 멈추는 동작과 관련된다.
Ox86 CPU의 Trap Flag(TF)를 켜서 하드웨어 단의 단일 스텝 디버깅을 구현합니다.
11

실행 파일 형식: ELF만 있는 것이 아니다linux_binfmt / binfmt_misc

리눅스 생태계를 지배하는 표준 실행 파일 포맷은 단연 ELF(Executable and Linking Format)입니다. 과거 유닉스 초창기의 a.out 형식을 밀어내고 사실상의 지배자가 되었죠. 하지만 놀랍게도 리눅스는 커널 차원에서 ELF 외에도 수많은 파일 형식을 품을 수 있도록 설계되어 있습니다.

linux_binfmt 객체 핵심 메서드
load_binary
해당 형식의 실행 파일을 파싱해서 현재 프로세스 위에 적절한 메모리 구조를 세팅합니다.
load_shlib
동적 라이브러리를 프로세스의 주소 공간에 로드하고 연결해 줍니다.
core_dump
프로세스가 비정상 종료(Segmentation Fault 등)될 때, 당시의 처참한 메모리 상태를 코어 파일로 남깁니다.

커널은 자신이 지원할 수 있는 포맷 목록(linux_binfmt 객체들)을 연결 리스트로 가지고 있습니다. 셸 스크립트나 파이썬 파일의 맨 윗줄에 적힌 #!/bin/bash(shebang)도 커널의 인터프리터 포맷 핸들러가 인식하여, 스크립트 대신 지정된 인터프리터를 실행하게끔 방향을 틀어줍니다.

나아가 binfmt_misc는 사용자가 원하는 커스텀 실행 형식을 커널에 직접 등록할 수 있게 해주는 아주 훌륭한 확장 기능입니다. 예를 들어 "파일 맨 앞의 매직 넘버가 'MZ'로 시작하는 윈도우용 exe 파일이면, 묻지도 따지지도 말고 /usr/bin/wine을 실행해서 넘겨줘"라는 식의 영리한 연결이 가능합니다.

커널이 실행 파일을 받아들면 마치 출입국 관리소 직원처럼 여권(헤더)을 살펴봅니다. 커널 내의 포맷 핸들러(직원)들을 한 명씩 부르며 "혹시 이 파일 해석할 줄 아는 사람?" 하고 묻습니다. ELF 창구, 스크립트 창구를 거쳐 아무도 나서는 사람이 없으면 -ENOEXEC(실행 형식 인식 실패) 도장을 쾅 찍고 쫓아냅니다.

핵심 O/X 퀴즈

1. 리눅스의 표준 실행 파일 형식은 ELF이다.
O유닉스와 리눅스를 관통하는 가장 핵심적이고 현대적인 포맷입니다.
2. linux_binfmt는 실행 파일 형식을 커널 내부에서 표현하는 구조와 관련된다.
O새로운 형식이 추가될 때마다 이 구조체를 커널 리스트에 등록하게 됩니다.
3. #!로 시작하는 스크립트는 커널이 항상 CPU 기계어로 직접 실행한다.
X커널은 기계어가 아님을 깨닫고 지정된 인터프리터(bash, python)를 로드해 스크립트를 인자로 던져줍니다.
12

Execution domain과 personality: 다른 유닉스 프로그램 실행하기Table 20-6

리눅스는 기계어 아키텍처(예: x86)만 같다면, 다른 운영체제용으로 컴파일된 이기종 바이너리도 실행할 수 있는 놀라운 유연성을 제공합니다. 이를 지원하는 방법은 크게 두 갈래로 나뉩니다.

다른 OS 프로그램 실행 방식
emulated execution Wine, DOSemu 계열

Windows나 MS-DOS처럼 시스템 콜 구조가 완전히 다른 외계의 프로그램들은 에뮬레이터의 도움을 받아야 합니다. 프로그램의 API 호출을 가로채어 리눅스가 알아들을 수 있는 시스템 콜로 통역(변환)해 줍니다.

native execution POSIX 준수 유닉스 계열

BSD나 Solaris 같이 POSIX 표준을 지키는 핏줄이 같은 유닉스 계열 프로그램들은 커널 단에서 직접 안고 갈 수 있습니다. 시스템 콜 번호나 시그널 체계의 미세한 방언(사투리) 차이 정도만 커널이 눈치껏 맞춰줍니다.

커널은 이를 관리하기 위해 execution domain이라는 개념을 도입했습니다. 프로세스는 자신의 personality 값을 조작해 "나는 지금부터 32비트 환경에 있다고 믿을래(PER_LINUX32)"라거나 "나는 SVR4 유닉스 시스템의 관습을 따를래"라고 커널에 선언할 수 있습니다.

외국 손님을 호텔에 맞이하는 것과 같습니다. 같은 규격의 침대를 제공하지만, 영국 손님이 오면 220V 플러그 어댑터를 미리 끼워두고 영국식 조식을 준비합니다. execution domain은 "이 손님이 편안함을 느끼기 위해 우리가 맞춰줘야 할 그 나라의 관습 세트"에 해당합니다.

핵심 O/X 퀴즈

1. Windows 프로그램처럼 리눅스 API와 다른 API를 쓰는 프로그램은 에뮬레이터의 도움이 필요할 수 있다.
O시스템 콜의 철학 자체가 다르기 때문에 Wine 같은 통역가가 필수적입니다.
2. execution domain은 프로세스가 기대하는 시스템 콜 방식이나 signal 번호 차이를 다루는 데 관련된다.
O유닉스 계열끼리의 미묘한 사투리 차이를 커널 레벨에서 보정해 줍니다.
3. personality 값은 파일 디스크립터 번호를 저장하는 배열이다.
X프로세스가 운영체제로부터 기대하는 '실행 환경의 성격(관습)'을 정의하는 플래그입니다.
13

exec 함수 가족: 이름에 담긴 규칙Table 20-7

유닉스 환경에는 execl(), execvp(), execve() 등 다채로운 exec 함수 패밀리가 존재합니다. 이들은 모두 현재의 프로세스 문맥을 지우고 새 파일을 덮어씌운다는 본질적인 목적은 같지만, 프로그래머가 인자를 던져주는 스타일의 차이일 뿐입니다.

Table 20-7 — exec 함수 이름 접미사의 의미
l (list)
가변 인자 리스트 방식으로 전달합니다. "ls", "-l", NULL 처럼 콤마로 나열합니다.
v (vector)
포인터 배열(vector)에 인자들을 예쁘게 담아서 배열의 시작 주소 하나만 던져줍니다.
p (PATH 검색)
절대/상대 경로(/)를 주지 않고 파일명만 줘도, 환경 변수 PATH를 뒤져서 알아서 파일을 찾아주는 친절한 옵션입니다.
e (environment)
새 프로그램이 사용할 환경 변수 배열을 사용자가 명시적으로 꽂아 넣습니다.

여기서 중요한 커널의 비밀이 하나 있습니다. 리눅스 커널이 실제로 제공하는 시스템 콜은 오직 execve() 단 하나뿐이라는 사실입니다. 나머지 모든 함수들은 C 표준 라이브러리(glibc)가 프로그래머의 편의를 위해 포장해 놓은 래퍼(Wrapper)에 불과합니다. 결국 내부적으로는 모두 인자를 다듬어 execve()를 호출하게 됩니다.

쉘(Shell) 프로그래밍을 할 때 보통 execvp()를 가장 많이 애용합니다. 왜냐하면 사용자가 경로 없이 cat이라고만 쳐도 알아서 PATH를 찾아줘야 하고, 터미널에서 쪼개진 인자들은 이미 띄어쓰기 기준으로 배열(Vector)에 담겨 있기 때문이죠.

핵심 O/X 퀴즈

1. execvp()p는 PATH 환경 변수를 이용한 검색과 관련된다.
O직접 경로를 찾을 필요가 없어 매우 편리한 옵션입니다.
2. execl() 계열의 l은 명령행 인자를 배열 하나로 전달한다는 뜻이다.
X배열은 v(vector)입니다. l(list)은 함수 인자로 쉼표를 찍어 길게 나열하는 방식입니다.
3. 리눅스에서 커널이 직접 제공하는 핵심 exec 시스템 콜은 execve()이다.
O결국 모든 길은 execve()로 통합니다. 나머지는 모두 래퍼 함수입니다.
14

do_execve() 전반부: 파일을 찾고, 검사하고, 형식을 고른다linux_binprm / search_binary_handler

sys_execve() 시스템 콜이 호출되면, 유저 공간의 데이터를 안전한 커널 공간으로 복사해온 뒤 핵심 코어 로직인 do_execve()로 바통을 넘깁니다. 전반부는 철저한 조사와 세팅의 과정입니다.

1
작업 공간 준비. 새 프로그램의 정보를 담을 거대한 서류철인 linux_binprm 구조체를 할당받습니다.
2
VFS 파일 탐색. 디스크 상의 실행 파일을 찾아 dentry, inode, file 객체를 확보합니다.
3
실행 권한 검사 및 쓰기 락. 권한이 있는지 살피고, 실행 도중 누군가 이 바이너리를 변조하지 못하도록 i_writecount를 조작해 파일 수정을 원천 봉쇄합니다.
4
prepare_binprm(). (중요) setuid 비트 등을 계산하여 프로세스의 신분(euid)을 어떻게 승격할지 결정하고, 파일의 맨 앞 128바이트를 쪽쪽 빨아들여 버퍼에 담아둡니다.
5
search_binary_handler(). 앞서 읽은 128바이트(매직 넘버 포함)를 들고, 커널 내의 포맷 핸들러들을 순회하며 "이 포맷 해석할 줄 아는 모듈?" 하고 물어봅니다.

파일의 맨 앞 128바이트가 핵심인 이유는 바로 거기에 '내가 어떤 포맷이다'라는 매직 넘버(예: ELF의 \x7fELF)가 박혀 있기 때문입니다. 이 앞부분을 읽지 않고서는 이 파일이 윈도우용인지, 스크립트인지, 리눅스 바이너리인지 판단할 길이 없습니다.

정리하자면, exec의 전반부는 본격적인 실행에 앞서 철저한 신원 확인과 입국 심사를 거치는 과정입니다. 파일이 존재하는지, 권한은 올바른지, 실행 중 훼손될 위험은 없는지, 심지어 어떤 언어로 쓰인 파일인지 이 모든 준비가 무결하게 끝나야 비로소 다음 단계인 메모리 덮어쓰기로 넘어갈 수 있습니다.

핵심 O/X 퀴즈

1. linux_binprm은 새 실행 파일 처리에 필요한 정보를 담는 자료구조로 사용된다.
Oexec 과정 전체를 들고 다니는 핵심 작업지시서와 같습니다.
2. search_binary_handler()는 formats 리스트를 훑으며 적절한 실행 파일 형식 처리기를 찾는다.
O적합한 포맷을 만날 때까지 핸들러들의 load_binary 함수를 찔러봅니다.
3. 실행 파일의 처음 128바이트는 아무 의미가 없어서 커널이 읽지 않는다.
X파일의 운명을 결정짓는 매직 넘버가 바로 이 앞부분에 모여 있습니다.
15

load_binary()와 되돌아갈 수 없는 지점flush_old_exec / setup_arg_pages

알맞은 형식의 load_binary 모듈(예: ELF 핸들러)이 호출되면, 헤더를 파싱해 텍스트와 데이터가 어느 주소에 매핑되어야 하는지, 동적 링커(ld.so)가 필요한지 파악합니다. 그리고 대망의 flush_old_exec()를 호출합니다.

이 순간이 바로 루비콘강을 건너는 '되돌아갈 수 없는 지점(Point of No Return)'입니다. 이 강을 건너고 나면, 설령 이후 단계에서 에러가 발생하더라도 이전 셸 상태로 절대 돌아갈 수 없습니다. 이미 다 부숴버렸기 때문이죠.

flush_old_exec() 내부에서 일어나는 일 (파괴 공작)
시그널 초기화
기존 프로그램이 달아두었던 시그널 핸들러들을 모조리 떼어버리고 기본값으로 리셋합니다.
메모리 맵핑 해제
기존에 매핑되어 있던 텍스트, 데이터, 힙 공간을 무자비하게 날려버립니다.
TSS 정리
레지스터 상태나 부동 소수점 연산 잔여물 등 과거의 기억을 완전히 지웁니다.
FD 정리
close-on-exec 플래그가 붙은 열린 파일들을 일제히 닫아버립니다.

깔끔하게 철거가 끝난 공터에 이제 새 건물을 올립니다.

스택 세팅. setup_arg_pages()를 통해 새 스택의 뼈대를 잡고, 앞서 피신시켜 둔 명령행 인자와 환경 변수를 최상단에 이쁘게 꽂아 넣습니다.
코드와 데이터 매핑. do_mmap()을 이용해 실행 파일의 코드(text)와 데이터 영역을 메모리 주소 공간에 얹습니다.
BSS 할당. 0으로 초기화되어야 할 BSS 영역을 확보하기 위해 동적 익명 매핑(brk)을 설정합니다.
동적 링커 매핑. 만약 동적 라이브러리를 쓰는 프로그램이라면, 커널이 직접 동적 링커(ld.so)를 찾아 빈 공간에 올려줍니다.
start_thread(). 커널 모드에서 유저 모드로 돌아갈 때 CPU가 어디서부터 시작할지 가리키는 eip(PC)를 동적 링커의 진입점(또는 프로그램의 메인)으로 덮어치고, esp(스택 포인터)를 새 스택의 꼭대기로 맞춰줍니다.

시스템 콜이 끝나고 유저 모드로 복귀하는 순간, CPU는 완전히 낯선 명령어 주소(변경된 eip)로 점프하게 됩니다. 완벽한 신분 세탁이자 새로운 세계의 시작입니다.

핵심 O/X 퀴즈

1. flush_old_exec() 이후에는 기존 실행 문맥 대부분이 정리되어 이전 계산으로 복구하기 어렵다.
Oexec 과정 중 되돌아갈 수 없는 파괴의 분기점입니다.
2. setup_arg_pages()는 새 사용자 모드 스택 영역과 인자·환경 문자열 배치에 관여한다.
O새로운 스택 환경을 구축하는 핵심 함수입니다.
3. start_thread()는 새 프로그램 실행을 위해 사용자 모드 eipesp 값을 설정하는 과정과 관련이 없다.
X가장 중요한 작업입니다. 이 레지스터 조작 덕분에 돌아갈 목적지가 완전히 뒤바뀌게 됩니다.
16

동적 링커 마무리와 전체 흐름 정리DYNAMIC LINKER → main()

드디어 execve() 시스템 콜이 끝났습니다! 새로운 text, bss, stack이 깔렸으니 이제 곧바로 main() 함수가 실행될까요? 동적 링크로 컴파일된 프로그램이라면 대답은 '아니요'입니다.

커널이 방금 설정해 둔 eipmain()이 아니라 동적 링커(dynamic linker)의 진입점을 가리키고 있습니다. 유저 모드로 돌아오면 가장 먼저 링커 코드가 깨어나 뒷정리를 시작합니다.

1
상황 파악. 커널이 스택에 남겨둔 보조 벡터(auxiliary vector)를 읽어 자신이 어디에 로드되었는지 확인합니다.
2
의존성 스캔. 본 프로그램이 구동되기 위해 어떤 `.so` 공유 라이브러리들이 필요한지 분석합니다.
3
메모리 매핑. 필요한 라이브러리 파일을 찾아서 mmap()으로 프로세스 공간 내에 줄줄이 엮어 넣습니다.
4
심볼 해결(Relocation). 곳곳에 구멍 나 있던 라이브러리 함수(예: printf)들의 가상 주소를 찾아 실제 호출 위치를 꼼꼼히 꿰매어 연결합니다.
5
대망의 main() 진입. 모든 의존성이 완벽하게 연결되면, 드디어 제어권을 진짜 프로그램의 시작점(main)으로 넘깁니다. 바야흐로 온전한 실행이 시작됩니다.

반면 정적 링크 프로그램의 경우는 훨씬 단순하게 흘러갑니다. 외부 라이브러리에 빚진 것이 없으니 이 복잡한 동적 링커 단계를 거칠 필요가 없습니다. 커널이 곧바로 eip를 타겟 프로그램의 main()으로 꽂아주어 실행이 즉시 시작됩니다.

전체 흐름을 요약해 볼까요?
사용자가 터미널에 명령어 입력 → 셸이 fork()로 분신 생성 → 자식 프로세스가 execve() 호출 → 커널 진입 후 파일 탐색 및 권한/포맷 검증 → flush_old_exec()로 옛 문맥 폐기 → 새 메모리 구역 도면 설계 및 맵핑 → 레지스터(eip/esp) 교체 후 유저 모드 복귀 → 동적 링커가 구동되어 심볼 연결 → 드디어 main()으로 점프!

execve()는 완벽한 리모델링입니다. 집 주소(PID)는 그대로인데, 벽지부터 가구, 전기 배선, 수도관까지 통째로 갈아엎습니다. 공사가 끝난 뒤 현관문을 열고 나오는 사람은 예전 주인이 아니라 완전히 낯선 새 입주자입니다. 우리가 습관적으로 치는 ls 명령어 한 번의 이면에는, 이토록 우아하고 치열한 거대한 공사판이 수 밀리초 만에 벌어지고 있는 것입니다.

핵심 O/X 퀴즈

1. 동적 링크 프로그램에서는 커널이 준비를 마친 뒤 dynamic linker가 먼저 실행되어 공유 라이브러리를 매핑하고 심볼을 해결할 수 있다.
O동적 링커가 이 무거운 작업을 유저 모드에서 안전하게 처리합니다.
2. 정적 링크 프로그램도 반드시 dynamic linker가 모든 공유 라이브러리를 찾아야만 실행된다.
X정적 링크는 모든 코드를 내장하고 있으므로 동적 링커의 도움이 필요 없습니다.
3. 프로그램 실행 과정은 파일시스템, 권한, 메모리 관리, 실행 파일 형식, 동적 링크가 함께 얽힌 커널 작업이다.
O운영체제의 거의 모든 서브시스템이 교차하는 아름다운 종합 예술입니다.

프로그램 실행은 리눅스 커널의 파일시스템 · 권한 모델 · 메모리 관리 · 스케줄러 · 동적 링크가 한 번에 맞물려 돌아가는 경이로운 종합 무대입니다.

강의노트 — Linux Kernel, Chapter 20 "Program Execution" 기반 재구성