4 분 소요

elf_title_image.jpg

개요

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 배수 주소
특징 공간 효율 ↑, 관리 복잡 관리 단순, 대용량 처리 효율 ↑

참조

elf(5) - Linux manual page

카테고리:

업데이트: