ELF 정렬
개요
Android 15에서 16KB ELF 정렬을 사용해서 Android를 빌드할 수 있게 되었다. ELF 정렬이란 무엇일까?
ELF
ELF (Executable and Linkable Format)는 리눅스에서 실행 파일을 저장하는 파일 형식이다. (.exe
비슷한 개념)
즉, 프로그램이 실행되기 전 상태의 ‘완성된 포장 파일’ 이라고 보면 된다.
이 파일 안에는 코드, 데이터, 심볼, 섹션 정보 등이 정리되어 들어 있다.
정렬
정렬은 데이터를 메모리에서 효율적으로 배치하는 규칙
만약 4바이트짜리 데이터를 넣어야 하는데 주소가 1에서 시작하면 (1~4칸 차지) → CPU는 2번 접근해야 한다,
하지만 4의 배수 주소인 4번 칸에 맞춰 넣으면 한 번에 읽을 수 있다.
ELF 파일은 여러 섹션(section) 으로 구성된다.
예:
.text
→ 코드.data
→ 전역 변수.bss
→ 초기화되지 않은 변수
각 섹션은 메모리나 파일 내에서 특정 주소 단위(보통 4바이트, 8바이트 등)로 정렬(aligned) 되어 있어야 한다.
1️⃣ 이유 1: CPU 성능 향상
데이터가 정렬돼 있으면 CPU가 더 빠르게 읽고 쓸 수 있다.
(잘못 정렬되면 접근 시 두 번 읽거나, 예외가 발생할 수 있다.)
2️⃣ 이유 2: OS 로더의 요구사항
운영체제(커널)는 ELF를 메모리에 로드할 때 페이지 단위(보통 4KB)로 데이터를 올리기 때문에, 각 섹션이 페이지 경계에 맞게 정렬돼 있어야 한다.
3️⃣ 이유 3: 구조체 패딩과 비슷한 원리
C 구조체(struct)에서 멤버가 메모리 경계에 맞춰져야 하는 것과 같은 개념이다.
정렬 없이 그냥 순서대로 읽으면 안되는가?
CPU가 메모리를 읽는법
사실 CPU가 메모리를 읽는 방식 자체가 그렇게 단순하지 않다.
CPU는 메모리를 한 바이트씩이 아니라, 한 번에 2바이트(16비트), 4바이트(32비트), 8바이트(64비트) 단위로 읽는다. 이걸 버스 폭(bus width) 이라고 한다.
예를 들어 32비트 CPU는 한 번에 4바이트씩 읽을 수 있다.
메모리 구조를 그림으로 보면
주소: 0 1 2 3 | 4 5 6 7 | 8 9 10 11 ...
<--- 첫 번째 4바이트 --->
<--- 두 번째 4바이트 --->
- CPU는 보통 0~3, 4~7, 8~11 이렇게 4바이트 단위 블록으로 나누어 접근한다.
- 즉, “4칸 묶음” 단위로 읽는다.
정렬이 안되어있을 경우
예를 들어 4바이트짜리 데이터를 주소 1부터 저장했다고 하자.
주소 | 값 |
---|---|
0 | (다른 데이터) |
1 | A |
2 | B |
3 | C |
4 | D |
이때 CPU는 이렇게 해야 한다.
- 첫 번째 블록(0 ~ 3)에서 1 ~ 3번째 바이트(ABC) 읽고
- 두 번째 블록(4 ~ 7)에서 4번째 바이트(D) 읽고
- 그 둘을 합쳐서 하나의 값으로 만들어야 함
즉, 메모리를 두 번 읽고 조립해야한다.
이건 시간이 더 걸리고, 하드웨어 구조도 복잡해진다.
정렬이 되어 있을 경우
이제 4바이트 데이터를 주소 4부터 넣었다고 해보자
주소 | 값 |
---|---|
4 | A |
5 | B |
6 | C |
7 | D |
이 경우엔 CPU가 “0x4~0x7 블록”을 한 번에 딱 읽으면 끝이다.
정리
구분 | 정렬 안 됨 (주소 1부터) | 정렬 됨 (주소 4부터) |
---|---|---|
읽기 횟수 | 2번 | 1번 |
속도 | 느림 | 빠름 |
하드웨어 처리 | 복잡 | 단순 |
일부 CPU | 오류 발생 가능 | 정상 |
운영체제의 메모리 관리
주소:
0x00000000 ────────────────
0x00001000 ────────────────
0x00002000 ────────────────
0x00003000 ────────────────
↑
여기까지 4KB (4096바이트)
운영체제는 메모리를 4KB(=4096바이트) 단위로 잘라서 관리한다.
이 조각 하나를 페이지(page) 라고 한다.
📦 페이지는 일종의 “메모리 박스” 같다고 생각하면 된다.
한 박스당 4KB씩, 번호를 붙여서 쓴다.
운영체제가 이걸 메모리에 올릴 때는 “페이지 단위(4KB)”로 옮겨야 한다.
데이터 주소 경계가 어긋나면?
만약 코드가 이런 식으로 저장돼 있다고 해보자
코드(.text): [ 0x08048010 ~ 0x08049000 ]
데이터(.data): [ 0x08049000 ~ 0x08049800 ]
문제는 .text
가 0x08048010처럼 4KB의 배수가 아닌 어중간한 곳에서 시작하면, 운영체제가 이걸 로드할 때 이렇게 된다.
“어? 이 조각이 0x1000 단위(4KB)에 안 맞네?
그럼 두 개 페이지에 걸쳐 있으니까 두 번 읽어야겠다.”
즉,
- 한 페이지(4KB)에 딱 맞게 안 들어가면
- 로드가 느려지고, 관리가 복잡해진다.
그래서 정렬이 필요하다.
ELF는 이런 식으로 설정해둔다.
.text → 0x08048000 (4KB의 배수 주소!)
.data → 0x08049000 (그다음 페이지)
이렇게 맞춰놓으면 운영체제가 “페이지 단위로 딱딱 잘라서” 빠르고 깔끔하게 메모리에 올릴 수 있다.
📦 4KB 정렬 = “운영체제가 메모리를 4KB 박스 단위로 관리하니까, ELF의 조각들도 박스 경계에 맞춰 담는다.”
실제로 ELF 안에 이렇게 적혀 있다.
$ readelf -l a.out
LOAD 0x000000 0x08048000 0x08048000 0x001000 0x001000 R E 0x1000
맨 끝의 0x1000
= 정렬 크기(alignment)
→ 즉, “이 세그먼트는 반드시 4KB(=0x1000) 경계에 맞춰서 로드해라.”
“16KB 페이지 크기”는?
“16KB 페이지를 지원한다”는 건
운영체제가 메모리를 16KB(16384바이트) 단위로 자르고 관리할 수 있다는 뜻이다.
즉, 한 페이지가 4배 더 커진 것.
주소:
0x00000000 ~ 0x00003FFF → 1페이지 (16KB)
0x00004000 ~ 0x00007FFF → 2페이지 (16KB)
0x00008000 ~ 0x0000BFFF → 3페이지 (16KB)
...
왜 페이지 크기를 바꿀까?
페이지 크기에는 장단점이 있다.
페이지 크기 | 장점 | 단점 |
---|---|---|
4KB | 낭비가 적음 (작은 데이터도 효율적) | 페이지 수가 많아서 관리 비용이 큼 |
16KB | 관리가 단순해지고 캐시 효율 ↑ | 낭비가 커질 수 있음 (작은 데이터도 16KB 차지) |
예를 들어,
- 작은 파일 여러 개를 다루는 경우 → 4KB가 효율적
- 큰 프로그램이나 대용량 데이터 처리 → 16KB 페이지가 더 빠름
ELF 관점에서 보면?
ELF(실행 파일)는 “페이지 단위”로 메모리에 올라간다.
그래서 ELF에는 이런 정보가 들어 있다.
LOAD ... R E 0x1000
여기서 0x1000
= 4KB 정렬
즉, “이 프로그램은 4KB 단위 페이지에 맞춰서 로드돼야 한다”는 뜻이다.
그런데 만약 시스템이 16KB 페이지를 지원한다면 이 값은 이렇게 될 수 있다.
LOAD ... R E 0x4000
0x4000
= 16KB 정렬
즉, “이 프로그램은 16KB 단위 페이지 경계에 맞춰서 올려라.”
이 말은:
ELF의 세그먼트 시작 주소가 16KB의 배수여야 OS가 메모리에 올릴 때 정확히 맞아떨어진다는 뜻이다.
쉽게 비유하자면
비유 | 4KB 페이지 | 16KB 페이지 |
---|---|---|
📦 택배 상자 크기 | 작은 상자 | 큰 상자 |
📦 한 상자에 담을 수 있는 물건 | 4개 | 16개 |
📦 상자 관리 수 | 많음 | 적음 |
💰 상자 하나의 빈 공간 낭비 | 적음 | 많음 |
즉
“16KB 페이지를 지원한다” = “운영체제가 더 큰 박스로 메모리를 나눠 관리할 수 있다” 는 뜻이다.
정리
항목 | 4KB 페이지 | 16KB 페이지 |
---|---|---|
페이지 크기 | 4096바이트 | 16384바이트 |
정렬 단위 | 0x1000 | 0x4000 |
ELF 정렬 시 | 4KB 배수 주소 | 16KB 배수 주소 |
특징 | 공간 효율 ↑, 관리 복잡 | 관리 단순, 대용량 처리 효율 ↑ |