실행 파일
고친 사람 github-actions[bot]
실행 파일은 이름만 대면 운영체제가 바로 돌려 주는 파일입니다. 사람이 짠 코드를 기계가 읽는 명령으로 바꿔 담습니다. 그 명령을 메모리 어디에 올리고 어디서부터 시작할지까지 같이 적어 둡니다. 그래서 컴퓨터는 이 파일 하나만 받으면 프로그램을 켤 수 있습니다.
쉽고 빠른 이해
실행 파일은 운영체제가 받아서 바로 돌릴 수 있게 만들어 둔 파일입니다. 리눅스에서 ls 라고 치면 도는 것이 그런 파일 하나입니다.
이게 없으면 프로그램을 켤 때마다 소스 코드를 그 컴퓨터에서 다시 번역해야 합니다. 번역에 쓰는 도구도 돌릴 컴퓨터마다 깔려 있어야 합니다.
어떻게 도나:
- 컴파일러와 링커가 소스 코드를 기계 명령으로 바꿔 파일 하나에 담습니다
- 그 파일 앞머리에 무엇을 메모리 어디에 올릴지와 첫 명령이 어디인지를 적습니다
- 켜면 운영체제가 앞머리를 읽어 내용을 메모리에 올리고 첫 명령으로 넘어갑니다
대가는 묶인다는 것입니다. 만들 때 정한 기계와 운영체제에서만 돕니다. 사람이 열어 읽기도 어렵습니다.
상세
조립까지 마쳐 상자에 담아 둔 가구와 같습니다. 받는 사람은 공구를 꺼낼 일이 없습니다. 상자를 열어 놓기만 하면 됩니다.
이 절은 어떤 파일이 실행 파일이 되는지, 그 안에 무엇이 들어 있는지, 켤 때 무슨 일이 벌어지는지를 차례로 봅니다.
실행 파일이 되는 두 조건
파일 이름은 조건이 아닙니다. 끝에 무엇이 붙어 있든 운영체제는 파일 안을 들여다보고 판단합니다.
첫째 조건은 운영체제가 아는 형식으로 적혀 있어야 한다는 것입니다. 실행 파일은 앞머리 몇 바이트에 자기 형식을 알리는 표시를 답니다. 이 표시가 매직 넘버입니다. 운영체제는 이 몇 바이트만 읽고도 켤 수 있는 파일인지 가려냅니다.
둘째 조건은 실행 권한입니다. 유닉스 계열 운영체제는 파일마다 읽기·쓰기·실행 권한을 따로 답니다. 형식이 맞아도 실행 권한이 없으면 운영체제의 핵심부인 커널이 켜기를 거부합니다. chmod +x 가 이 권한을 붙이는 명령입니다.
두 조건을 다 갖춘 파일을 만들어 내는 것이 빌드입니다. 컴파일러가 소스 파일을 기계 명령으로 옮깁니다. 링커가 그 결과를 이어 붙여 파일 하나로 만듭니다.
번역을 미리 끝내 두는 것이 이 파일의 존재 이유입니다. 돌릴 컴퓨터에는 소스 코드도 컴파일러도 없어도 됩니다. 이 파일 하나만 있으면 프로그램이 돕니다.
파일 안에 담기는 덩이
실행 파일은 한 덩이처럼 보이지만 안은 성격이 다른 구역으로 나뉩니다. 맨 앞에 오는 것이 헤더입니다. 앞에서 앞머리라 부른 것이 이 헤더입니다.
헤더는 본문을 읽기 전에 먼저 읽는 안내문입니다. 이것이 없으면 어느 바이트가 명령이고 어느 바이트가 데이터인지 가릴 길이 없습니다.
헤더 다음에 실제 내용이 옵니다. 기계가 읽을 명령을 모아 둔 구역과, 프로그램이 쓸 값을 미리 적어 둔 구역이 따로 놓입니다. 앞을 코드 구역, 뒤를 데이터 구역이라고 부릅니다. 이렇게 파일 안에서 종류별로 갈라 둔 구역 하나하나가 섹션입니다.
섹션들이 파일 안에 놓이는 순서는 아래와 같습니다. 맨 뒤에 붙는 심볼 테이블은 함수와 변수의 이름을 적어 둔 표입니다.
block-beta columns 1 H["헤더 · 무엇을 어디에 올릴지"] C["코드 섹션 · 기계가 읽는 명령"] D["데이터 섹션 · 미리 정해 둔 값"] S["심볼 테이블 · 이름과 번지의 짝"]
심볼 테이블은 그 이름 하나하나를 번지와 짝지어 둡니다. 번지는 메모리 안에서 그것이 놓인 곳을 가리키는 숫자입니다. 프로그램을 돌리는 데 이 표는 없어도 됩니다. 디버거가 오류 난 번지를 함수 이름으로 바꿔 보여줄 때 씁니다.
배포할 때는 이 구역을 떼어내 파일을 줄이기도 합니다. 떼어내는 작업이 스트립입니다.
섹션들을 메모리에 올릴 단위로 묶은 것은 세그먼트입니다. 섹션이 링커가 쓰는 눈금이라면 세그먼트는 로더가 쓰는 눈금입니다. 헤더는 세그먼트 하나하나를 어느 번지에 올릴지 적어 둡니다.
묶는 잣대는 메모리에서 어떻게 다룰 것인가입니다. 값이 바뀌지 않는 읽기 전용 데이터는 코드와 한 세그먼트가 됩니다. 프로그램이 고쳐 쓸 데이터는 다른 세그먼트로 갑니다. 심볼 테이블은 어느 쪽에도 안 들어갑니다.
flowchart TD
subgraph SEG1["읽고 실행하는 세그먼트"]
C["코드 섹션"]
R["읽기 전용 데이터 섹션"]
end
subgraph SEG2["읽고 쓰는 세그먼트"]
D["데이터 섹션"]
end
S["심볼 테이블 · 어느 세그먼트에도 안 들어간다"]
SEG1 --> M["메모리 · 헤더가 적어 둔 번지"]
SEG2 --> M
켜는 순간 벌어지는 일
셸은 사람이 친 명령을 받아 프로그램을 대신 켜 주는 프로그램입니다. 셸에 프로그램 이름을 치면 셸은 그 이름을 커널에게 넘깁니다. 켜는 일을 셸이 직접 하지는 않습니다. 파일을 메모리에 올리는 권한은 운영체제에게만 있습니다.
커널은 파일 헤더를 읽어 형식을 확인합니다. 아는 형식이면 로더에게 넘깁니다. 로더는 헤더가 시킨 대로 코드와 데이터를 메모리에 올립니다.
올리기를 마치면 헤더에 적힌 진입점으로 넘어갑니다. 진입점은 프로그램의 첫 명령이 놓인 번지입니다. 이 번지로 넘어간 순간부터 파일은 돌고 있는 프로세스가 됩니다.
바깥의 라이브러리를 빌려 쓰는 실행 파일은 한 단계를 더 거칩니다. 빌려 쓸 함수의 번지를 적을 칸은 빌드할 때 빈칸으로 남습니다. 그 함수가 메모리 어느 번지에 올라올지는 켜 봐야 알기 때문입니다.
동적 링커가 필요한 공유 라이브러리를 찾아 같이 올립니다. 그러고 나서 그 라이브러리의 함수 번지를 빈칸에 채워 넣습니다. 이 방식이 동적 링크입니다.
이름을 친 순간부터 프로그램이 돌기까지를 한 줄로 세우면 아래와 같습니다.
flowchart TD
A["셸 · 이름을 넘긴다"] --> B["커널 · 헤더를 읽어 형식을 본다"]
B --> C["로더 · 코드와 데이터를 메모리에 올린다"]
C -->|"빌려 쓴 라이브러리가 있으면"| L["동적 링커 · 같이 올리고 빈칸을 채운다"]
C -->|"빌려 쓴 라이브러리가 없으면"| D["진입점 · 첫 명령"]
L --> D
D --> E["프로세스 · 프로그램이 돈다"]
옮겨 돌 수 있는 범위
실행 파일은 두 가지에 묶입니다. 어느 기계용인가와 어느 운영체제용인가입니다.
먼저 기계입니다. 기계어 명령은 프로세서 종류마다 생김새가 다릅니다. 노트북용으로 만든 파일을 휴대폰 프로세서에 올리면 바이트는 읽히지만 명령으로는 뜻이 통하지 않습니다.
다음은 운영체제입니다. 운영체제마다 아는 형식이 다릅니다. 헤더의 생김새와 구역을 나누는 방법이 갈립니다.
| 운영체제 | 실행 파일 형식 |
|---|---|
| 리눅스 · 유닉스 계열 | ELF(Executable and Linkable Format) |
| 윈도 | PE(Portable Executable) |
| macOS | Mach-O |
그래서 같은 프로그램이라도 돌릴 곳마다 따로 빌드합니다. 한 컴퓨터에서 다른 기계나 다른 운영체제용 실행 파일을 만드는 일이 크로스 컴파일입니다.
빌린 라이브러리도 옮기는 데 걸림돌이 됩니다. 받는 컴퓨터에 그 라이브러리가 없으면 프로그램은 켜지지 않습니다. 라이브러리 코드를 파일 안에 미리 복사해 넣는 정적 링크를 고르면 파일 하나만 옮겨도 됩니다.
스크립트 파일과 갈리는 선
파이썬 파일이나 셸 스크립트도 이름을 치면 돕니다. 그런데 그 파일 안에는 기계 명령이 없습니다. 사람이 읽는 글자뿐입니다.
유닉스 계열은 이런 파일 첫 줄에 해석기의 경로를 적어 두는 방법을 씁니다. 커널은 그 표시를 보면 파일을 직접 돌리지 않습니다. 적힌 해석기를 대신 켜고 그 파일을 넘깁니다. 켜지는 실행 파일은 해석기 쪽입니다.
자바나 파이썬이 만들어 내는 바이트코드도 실행 파일이 아닙니다. 기계가 아니라 가상 머신이 읽는 명령을 담고 있습니다. 운영체제가 직접 못 켭니다. 켜지는 것은 여기서도 가상 머신 쪽입니다.
커널이 앞머리를 읽고 갈리는 세 경우를 한 그림에 모으면 아래와 같습니다.
flowchart TD
K["커널 · 파일 앞머리를 읽는다"] --> A["아는 형식의 기계 명령"]
K --> B["첫 줄에 적힌 해석기 경로"]
K --> C["가상 머신이 읽는 바이트코드"]
A --> P["그대로 메모리에 올라 프로세스가 된다"]
B --> I["해석기나 가상 머신이 켜지고 이 파일은 입력이 된다"]
C --> I
가르는 잣대는 하나입니다. 운영체제가 이 파일을 받아 그대로 메모리에 올릴 수 있나입니다. 다른 프로그램의 손을 빌려야 도는 파일은 입력이지 실행 파일이 아닙니다.
관련 항목
실행 파일을 만들어 내는 빌드 도구
컴파일러 · 어셈블러 · 링커 · 정적 링커 · 빌드 시스템
실행 파일이 만들어지기까지 거치는 단계
컴파일 · 어셈블 · 정적 링크 · 동적 링크 · 재배치 · 크로스 컴파일
실행 파일을 담는 파일 형식
ELF · PE · Mach-O · COFF · a.out
실행 파일 안에 들어 있는 구성 요소
헤더 · 섹션 · 세그먼트 · 진입점 · 심볼 테이블 · 디버그 심볼 · 매직 넘버 · 기계어
실행 파일을 메모리에 올리는 구성 요소
로더 · 동적 링커 · 커널 · 프로세스 · 프로세스 이미지 · 가상 메모리
실행 파일을 켜고 다루는 명령과 호출
셸 · execve · chmod · 스트립 · 시스템 콜
실행 파일과 견주게 되는 다른 파일
목적 파일 · 공유 라이브러리 · 정적 라이브러리 · 스크립트 · 바이트코드 · 클래스 파일
실행 파일을 켤 때 나는 오류
ENOEXEC · EACCES · 미정의 참조 · 세그멘테이션 폴트 · 의존성 지옥
실행 파일에 걸리는 권한과 보안 장치
다른 이름: executable · executable file · 실행 가능 파일 · 실행 이미지