GNU Assembler (as) 완전 가이드
GAS 문법을 단순 명령어 목록이 아닌 커널 코드 작성 관점으로 설명합니다. `.section`·`.type`·`.size`·재배치(Relocation) 지시자 의미, 매크로(Macro)/조건 조립으로 반복 코드 관리, CFI 기반 언와인드 정보 생성, x86·AArch64·RISC-V별 calling convention 차이, C 코드 inline asm과의 제약 조건 연결, 부트 코드·트랩 핸들러(Handler)·저수준 컨텍스트 전환 루틴 작성 시 흔한 오류와 점검법까지 상세히 정리합니다.
핵심 요약
- 역할 —
as는.s/.S파일을 ELF.o로 변환하는 GNU 어셈블러입니다. GCC가 내부적으로 호출합니다. - 문법 — 기본은 AT&T 문법(소스→목적 순서, 접미사).
.intel_syntax noprefix로 Intel 문법 전환 가능. - 지시자 —
.section,.global,.type,.align등 100개+ 지시자로 어셈블 동작을 제어합니다. - CFI —
.cfi_startproc/.cfi_endproc으로 DWARF 언와인드 정보를 생성, 스택 트레이스와 예외 처리에 필수입니다. - 아키텍처 — x86/x86_64, AArch64, RISC-V 각각 고유 지시자와 확장 문법이 있습니다.
단계별 이해
- 기본 변환 이해
as -o foo.o foo.s로 어셈블리 파일을 오브젝트로 변환하고objdump -d foo.o로 결과를 확인합니다. - 문법 규칙 익히기
AT&T 문법의 레지스터(Register) 접두사(%), 상수 접두사($), 명령어 접미사(b/w/l/q)를 익힙니다. - 지시자 활용
.section으로 섹션을 지정하고,.global/.type/.size로 심볼 속성을 설정합니다. - CFI 추가
함수에.cfi_startproc/.cfi_endproc을 추가하여 DWARF 언와인드 정보를 생성합니다. - 아키텍처 확장
필요한 아키텍처(-march=)와 확장(+sve등)을 지정하여 최신 명령어를 활용합니다.
GNU Assembler 개요
GNU Assembler(as)는 GNU Binutils 패키지의 핵심 구성 요소로, 어셈블리 언어 소스 파일(.s, .S)을 ELF(Executable and Linkable Format) 오브젝트 파일(.o)로 변환합니다. GCC 컴파일 파이프라인에서 cc1(C 컴파일러 프론트엔드)이 생성한 임시 .s 파일을 as가 어셈블합니다.
as는 AT&T 문법을 기본으로 하지만, .intel_syntax 지시자로 Intel 문법으로 전환할 수 있습니다. 멀티패스 어셈블러이며, 전방 참조(forward reference)를 허용합니다. 전처리(#include, #define 등)는 as 자체가 하지 않고, C 전처리기(cpp)가 먼저 처리한 후 as에 전달합니다.
커맨드라인 옵션
GNU Assembler 2.46의 주요 커맨드라인 옵션입니다. 아키텍처 공통 옵션을 다루며, 아키텍처별 옵션은 각 섹션을 참고하세요.
기본 사용법
# 어셈블리 파일을 오브젝트로 변환
as -o output.o input.s
# 64비트 모드 (x86-64)
as --64 -o foo.o foo.s
# 32비트 모드 (x86)
as --32 -o foo.o foo.s
# 디버그 정보 포함 (DWARF)
as -g -o foo.o foo.s
# 크로스 어셈블러 (AArch64 타겟)
aarch64-linux-gnu-as -o foo.o foo.s
옵션 전체 참조
| 옵션 | 설명 |
|---|---|
-a[cdghilns][=file] | 어셈블 리스팅 생성. c=조건부 생략, d=디버그 생략, g=일반 정보, h=헤더, i=입력, l=리스팅, m=매크로, n=폼 피드 생략, s=심볼. =file로 파일 지정 |
--alternate | 대체 매크로 모드 활성화 (MRI 호환 매크로 확장) |
-D | 심볼 정의 (현재는 무시됨, 호환성 유지용) |
-f | 화이트스페이스/주석 전처리 생략. 입력이 이미 처리된 경우에 사용 |
-g | DWARF 디버그 정보 생성 (파일명, 라인 번호) |
-I path | .include 지시자의 검색 경로 추가 |
-K | 차이 테이블(difference table)이 넘치면 경고 출력 |
-L | 로컬 심볼(이름이 L로 시작)을 심볼 테이블에 포함 |
--listing-lhs-width=n | 리스팅 왼쪽 폭(바이트 수) 설정 |
--listing-rhs-width=n | 리스팅 오른쪽 폭(문자 수) 설정 |
-M / --mri | MRI 호환 어셈블러 모드 |
--MD file | Makefile 의존성 파일 생성 (gcc -MD와 유사) |
-o file | 출력 오브젝트 파일명 지정 (기본: a.out) |
-R | data 섹션을 text 섹션에 병합 (읽기 전용(Read-Only) 데이터 최적화) |
--statistics | 어셈블 완료 후 CPU 시간과 메모리 사용량 출력 |
--traditional-format | 일부 출력을 전통적(구식) 형식으로 출력 (호환성) |
-v / --version | 버전 정보 출력 후 종료 |
-W / --no-warn | 경고 메시지 억제 |
--warn | 경고 출력 활성화 (기본값) |
--fatal-warnings | 경고를 오류로 처리하여 어셈블 실패 |
-Z | 오류가 있어도 오브젝트 파일 생성 (불완전할 수 있음) |
리스팅 파일 활용
# 전체 리스팅 (주소, 인코딩, 소스 나란히)
as -aln=foo.lst -o foo.o foo.s
# 매크로 전개 포함
as -almn=foo.lst -o foo.o foo.s
# 의존성 파일 생성 (Makefile 통합)
as --MD foo.d -o foo.o foo.s
GAS 문법 규칙
GNU Assembler는 AT&T 문법을 기본으로 합니다. 소스 파일의 각 줄은 레이블, 명령어, 지시자, 또는 주석으로 구성됩니다.
기본 문법 구조
my_function:
movq $1, %rax
movl %eax, -4(%rbp)
ret
1:
decl %ecx
jnz 1b
jmp 2f
2:
주석 스타일
| 문법 | 설명 | 플랫폼 |
|---|---|---|
/* ... */ | C 스타일 블록 주석 | 모든 플랫폼 |
# 주석 | 줄 끝 주석 | 대부분 플랫폼 |
@ 주석 | 줄 끝 주석 | ARM |
// 주석 | C++ 스타일 줄 끝 주석 | 일부 아키텍처 |
; 주석 | 줄 끝 주석 | x86 일부 |
AT&T vs Intel 문법 비교
| 항목 | AT&T 문법 (기본) | Intel 문법 |
|---|---|---|
| 피연산자 순서 | 소스, 목적 (mov src, dst) | 목적, 소스 (mov dst, src) |
| 레지스터 | %rax, %rbx | rax, rbx |
| 상수(즉시값) | $42, $0x1f | 42, 0x1f |
| 명령어 접미사 | movb, movw, movl, movq | 접미사 없음 (크기 추론) |
| 메모리 참조 | offset(%base, %idx, scale) | [base + idx*scale + offset] |
| 전환 지시자 | (기본) | .intel_syntax noprefix |
.intel_syntax noprefix
mov rax, 1
mov [rbp-4], eax
.att_syntax prefix
상수 표현
| 타입 | 표기 | 예제 |
|---|---|---|
| 10진수 | 숫자 그대로 | 42, -100 |
| 16진수 | 0x 또는 0X 접두사 | 0xFF, 0x1000 |
| 8진수 | 0 접두사 | 0644, 077 |
| 2진수 | 0b 또는 0B 접두사 | 0b1010, 0B1111 |
| 문자 | 작은따옴표 | 'A, '\n |
| 현재 주소 | 점(.) | . - start_label |
심볼 명명 규칙
- 영문자, 숫자,
_,.,$로 구성 (숫자 시작 불가) L로 시작하는 심볼은 로컬(기본적으로 심볼 테이블에서 제외)- 숫자로만 이루어진 레이블은 지역(numeric local) 레이블로 재사용 가능 (
0:~9:) - 커널에서는 주로
ENTRY(name),END(name),SYM_FUNC_START(name)매크로 사용
섹션과 재배치
어셈블러는 소스를 여러 섹션으로 나눠 처리합니다. 각 섹션은 ELF 파일의 섹션 헤더로 기록되며, 링커(Linker)가 최종 실행 파일을 생성할 때 배치를 결정합니다.
표준 섹션
| 섹션 | 플래그 | 설명 |
|---|---|---|
.text | ax (실행·할당) | 실행 코드 섹션 |
.data | aw (쓰기·할당) | 초기화된 읽기/쓰기 데이터 |
.rodata | a (할당) | 읽기 전용 데이터 (상수) |
.bss | aw (쓰기·할당) | 미초기화 데이터 (파일에 공간 없음) |
.note | (없음) | GNU 노트 섹션 (빌드 ID 등) |
.debug_* | (없음) | DWARF 디버그 정보 |
섹션 제어 지시자
.section .text
.section .data
.section .mydata, "aw", @progbits
.section .mycode, "ax", @progbits
.pushsection .altinstructions, "a"
.long 0x1234
.popsection
.subsection 1
.previous
재배치(Relocation)
어셈블러가 심볼 주소를 확정할 수 없을 때 재배치 엔트리를 생성하며, 링커가 최종 주소를 채웁니다.
movq external_sym(%rip), %rax
call another_func
leaq my_data(%rip), %rsi
심볼과 속성
심볼은 주소·값에 이름을 붙이는 것입니다. .global/.local로 가시성, .type으로 타입, .size로 크기를 지정합니다.
심볼 가시성 지시자
| 지시자 | ELF 바인딩 | 설명 |
|---|---|---|
.global sym | STB_GLOBAL | 전역 심볼 — 다른 오브젝트 파일에서 참조 가능 |
.globl sym | STB_GLOBAL | .global의 동의어 |
.local sym | STB_LOCAL | 로컬 심볼 — 현재 오브젝트에서만 유효 |
.weak sym | STB_WEAK | 약한 심볼 — 강한 정의가 있으면 덮어써짐 |
.weakref alias, target | STB_WEAK | 약한 참조 별칭 정의 |
심볼 가시성(Visibility) 지시자
| 지시자 | ELF 가시성 | 설명 |
|---|---|---|
.protected sym | STV_PROTECTED | 외부에서 보이지만 오버라이드 불가 |
.hidden sym | STV_HIDDEN | 공유 라이브러리(Shared Library) 외부에서 보이지 않음 |
.internal sym | STV_INTERNAL | 프로세서별 숨김 (더 강한 제한) |
심볼 타입과 크기
.section .text
.global my_func
.type my_func, @function
my_func:
ret
.size my_func, .-my_func
.section .data
.global my_var
.type my_var, @object
my_var:
.long 42
.size my_var, 4
.symver my_func_v2, my_func@@GLIBC_2.17
상수 심볼
.equ PAGE_SIZE, 4096
.set STACK_SIZE, 0x4000
.equiv MAX_CPUS, 256
.equ BUFFER_END, BUFFER_START + BUFFER_SIZE
어셈블러 지시자 완전 참조
GAS는 100개 이상의 지시자를 제공합니다. 아래는 커널 개발에서 자주 사용하는 지시자를 카테고리별로 정리합니다.
섹션 관리
| 지시자 | 설명 |
|---|---|
.section name [, "flags" [, @type]] | 섹션 전환 또는 생성. 플래그: a(할당) w(쓰기) x(실행) M(병합) S(문자열) G(그룹) T(TLS) |
.text [n] | 코드 하위 섹션 n으로 전환 (기본 0) |
.data [n] | 데이터 하위 섹션 n으로 전환 |
.bss | BSS 섹션으로 전환 |
.pushsection name [, "flags"] | 현재 섹션을 스택에 저장하고 섹션 전환 |
.popsection | 스택에서 이전 섹션 복원 |
.previous | 최근 이전 섹션으로 복귀 |
.subsection n | 현재 섹션의 하위 섹션 n으로 전환 |
데이터 지시자
| 지시자 | 크기 | 설명 |
|---|---|---|
.byte expr [, ...] | 1바이트 | 1바이트 정수 상수 삽입 |
.word / .short expr | 2바이트 | 2바이트 정수 상수 |
.long / .int expr | 4바이트 | 4바이트 정수 상수 |
.quad expr | 8바이트 | 8바이트 정수 상수 |
.octa expr | 16바이트 | 16바이트 정수 상수 |
.float / .single expr | 4바이트 | IEEE 754 단정밀도 부동소수 |
.double expr | 8바이트 | IEEE 754 배정밀도 부동소수 |
.ascii "str" | 가변 | NULL 없는 문자열 |
.asciz "str" / .string "str" | 가변+1 | NULL 종료 문자열 |
.fill count [, size [, val]] | 가변 | count×size 바이트를 val로 채움. 기본: size=1, val=0 |
.space n [, val] / .skip n [, val] | n바이트 | n바이트 공간 확보, val로 채움 (기본 0) |
.zero n | n바이트 | n바이트 0으로 채움 (.space n, 0과 동일) |
.comm sym, size [, align] | 가변 | BSS 공통 심볼 선언 (링커가 병합) |
.lcomm sym, size [, align] | 가변 | 로컬 BSS 심볼 선언 |
.incbin "file" [, skip [, count]] | 가변 | 이진 파일을 현재 위치에 직접 삽입 |
정렬 지시자
| 지시자 | 설명 |
|---|---|
.align n [, fill [, max]] | n 바이트(또는 일부 아키텍처에서 2^n 바이트) 경계 정렬. fill: 패딩(Padding) 값, max: 최대 패딩 바이트 |
.balign n [, fill [, max]] | n 바이트 경계 정렬 (아키텍처 독립적, 이식성 우수) |
.p2align n [, fill [, max]] | 2^n 바이트 경계 정렬 (아키텍처 독립적) |
.align의 인자 해석이 아키텍처마다 다릅니다. x86에서는 바이트 수(.align 16 = 16바이트 정렬), ARM/RISC-V에서는 2의 지수(.align 4 = 16바이트 정렬). 이식성을 위해서는 .balign(바이트)이나 .p2align(지수) 사용을 권장합니다.
조건부 어셈블리
| 지시자 | 설명 |
|---|---|
.if expr | 표현식이 0이 아니면 포함 |
.ifz expr | 표현식이 0이면 포함 |
.ifeq expr | 표현식이 0이면 포함 (.ifz와 동일) |
.ifne expr | 표현식이 0이 아니면 포함 |
.ifgt / .ifge | 표현식이 0보다 크면 / 크거나 같으면 포함 |
.iflt / .ifle | 표현식이 0보다 작으면 / 작거나 같으면 포함 |
.ifdef sym | 심볼이 정의되어 있으면 포함 |
.ifndef sym / .ifnotdef sym | 심볼이 정의되지 않았으면 포함 |
.ifc str1, str2 | 두 문자열이 같으면 포함 |
.ifnc str1, str2 | 두 문자열이 다르면 포함 |
.else | 조건 반전 블록 |
.elseif expr | 추가 조건 |
.endif | 조건부 블록 종료 |
매크로 지시자
.macro SAVE_REGS reg1, reg2=rax, reg3:req
push \reg1
push \reg2
push \reg3
.endm
SAVE_REGS %rbx, %rcx, %rdx
.rept 4
nop
.endr
.irp reg, %rax, %rbx, %rcx
push \reg
.endr
.irpc c, abc
.byte '\c
.endr
.macro CHECK val
.if \val == 0
.exitm
.endif
.long \val
.endm
.purgem CHECK
기타 유용한 지시자
| 지시자 | 설명 |
|---|---|
.include "file" | 파일 내용 포함 (헤더 파일 삽입) |
.print "message" | 어셈블 중 메시지 출력 |
.warning "msg" | 경고 출력 (어셈블 계속) |
.error "msg" | 오류 출력 후 어셈블 중단 |
.abort | 즉시 어셈블 중단 |
.file "name" | 논리적 파일명 설정 (디버그 정보) |
.line n | 논리적 줄 번호 설정 |
.loc file line [col] | DWARF 위치 정보 설정 |
.org n [, fill] | 현재 섹션에서 오프셋(Offset) n으로 이동 |
.sleb128 expr | 부호 있는 LEB128 인코딩 정수 |
.uleb128 expr | 부호 없는 LEB128 인코딩 정수 |
.nops n [, max] | n바이트 NOP 패딩 (최적 NOP 선택) |
CFI 지시자 (DWARF 언와인드)
CFI(Call Frame Information) 지시자는 DWARF 언와인드 정보를 생성합니다. 스택 언와인드(예외 처리, backtrace(), perf callchain), 그리고 디버거가 콜 스택을 복원하는 데 필수적입니다. 리눅스 커널도 CONFIG_UNWINDER_FRAME_POINTER / CONFIG_UNWINDER_ORC 모드에서 CFI 정보를 활용합니다.
주요 CFI 지시자
| 지시자 | 설명 |
|---|---|
.cfi_startproc [simple] | CFI 정보 블록 시작. simple: 기본 초기화 생략 |
.cfi_endproc | CFI 정보 블록 종료 |
.cfi_def_cfa reg, offset | CFA = reg + offset으로 정의 |
.cfi_def_cfa_register reg | CFA 계산에 사용할 레지스터만 변경 (오프셋 유지) |
.cfi_def_cfa_offset offset | CFA 오프셋만 변경 (레지스터 유지) |
.cfi_adjust_cfa_offset n | 현재 CFA 오프셋에 n을 더함 |
.cfi_offset reg, offset | 레지스터 reg가 CFA+offset에 저장됨을 기록 |
.cfi_rel_offset reg, offset | 레지스터 reg가 현재 CFA 기준 offset에 저장됨 |
.cfi_register reg1, reg2 | reg1의 이전 값이 reg2에 있음을 기록 |
.cfi_restore reg | 레지스터 reg가 함수 진입 시 값으로 복원됨 |
.cfi_undefined reg | 레지스터 reg의 이전 값을 추적 불가 |
.cfi_same_value reg | 레지스터 reg 값이 변경되지 않음 |
.cfi_remember_state | 현재 CFI 상태를 스택에 저장 |
.cfi_restore_state | 스택에서 CFI 상태 복원 |
.cfi_return_column reg | 반환 주소 레지스터 지정 |
.cfi_signal_frame | 시그널(Signal) 프레임 표시 (특수 언와인드) |
.cfi_window_save | 레지스터 윈도우 저장 (SPARC) |
.cfi_escape expr [, ...] | 임의 DWARF CFA 표현식 삽입 |
.cfi_val_offset reg, offset | 레지스터의 현재 값 = CFA + offset |
.cfi_gnu_args_size size | GNU 확장: 피우시(pushed) 인자 크기 |
x86-64 함수 CFI 표준 패턴
.global example_func
.type example_func, @function
example_func:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
leave
.cfi_def_cfa %rsp, 8
ret
.cfi_endproc
.size example_func, .-example_func
커널 특수 CFI 패턴 (프레임 포인터 없는 경우)
.cfi_startproc
subq $8, %rsp
.cfi_adjust_cfa_offset 8
pushq %rbx
.cfi_adjust_cfa_offset 8
.cfi_rel_offset %rbx, 0
popq %rbx
.cfi_adjust_cfa_offset -8
.cfi_restore %rbx
addq $8, %rsp
.cfi_adjust_cfa_offset -8
ret
.cfi_endproc
x86/x86_64 아키텍처 기능
x86/x86_64 타겟에서 as의 특수 기능을 설명합니다.
x86 전용 옵션
| 옵션 | 설명 |
|---|---|
--32 / --x32 / --64 | 출력 코드 모드: 32비트 / x32 ABI / 64비트 |
-march=cpu | CPU 타입 지정 (generic32, generic64, x86-64, znver4, sapphirerapids 등) |
-mtune=cpu | 성능 최적화 타겟 CPU |
-msse2avx | SSE 명령어 인코딩을 VEX(AVX) 형식으로 출력 |
-mavxscalar=128 | AVX 스칼라 연산 크기 지정 |
-mno-shared | 공유 라이브러리 생성 안 함 (PLT/GOT 불필요) |
-mamd64 / -mintel64 | AMD64 vs Intel64 확장 선택 |
AT&T 문법 — 명령어 접미사와 메모리 참조
movb $0x41, %al
movw $0x1234, %ax
movl $0x12345678, %eax
movq $0x1234567890, %rax
movl (%rax), %ebx
movl 4(%rax), %ebx
movl (%rax, %rcx), %ebx
movl (%rax, %rcx, 4), %ebx
movl 8(%rax, %rcx, 4), %ebx
leaq my_data(%rip), %rsi
movq global_var(%rip), %rax
명령어 프리픽스
| 프리픽스 | 설명 |
|---|---|
lock | 원자적(Atomic) 메모리 연산 (메모리 버스(Bus) 잠금(Lock)) |
rep / repe / repne | 문자열 반복: 무조건 / ZF=1일 때 / ZF=0일 때 |
cs:, ds:, es:, fs:, gs:, ss: | 세그먼트 오버라이드 (Linux에서 fs:/gs:가 TLS용) |
data16 / addr16 | 32/64비트 모드에서 16비트 피연산자/주소 강제 |
rex64 | REX.W 프리픽스 명시적 지정 |
notrack | CET 간접 분기 추적 비활성화 |
x86 특수 지시자
.intel_syntax noprefix
mov rax, 1
add rbx, [rsp+8]
.att_syntax prefix
.byte 0x0f, 0x1f, 0x00
.byte 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00, 0, 0, 0, 0
endbr64
SYM_CODE_START(entry_SYSCALL_64)
.cfi_startproc simple
.cfi_signal_frame
.cfi_def_cfa rsp, 0
.cfi_register rip, rcx
.cfi_endproc
SYM_CODE_END(entry_SYSCALL_64)
AArch64 아키텍처 기능
GNU Assembler의 AArch64(ARMv8+) 지원 기능입니다.
AArch64 전용 옵션
| 옵션 | 설명 |
|---|---|
-march=armv8-a[+ext] | 아키텍처 버전 및 확장 지정 |
-mcpu=cpu | 특정 CPU (cortex-a55, cortex-a78, neoverse-n2 등) |
-mabi=ilp32 / -mabi=lp64 | ILP32 vs LP64 ABI 선택 |
-EB / -EL | 빅 엔디안(Endianness) / 리틀 엔디안 |
아키텍처 확장 (+extension)
| 확장 | 설명 |
|---|---|
+sve | Scalable Vector Extension (가변 길이 벡터) |
+sve2 | SVE2 (확장 벡터 명령어) |
+lse | Large System Extension (원자 연산 강화) |
+mte | Memory Tagging Extension |
+btc | Branch Target Identification |
+rcpc | Release-Consistent Processor Consistent 로드 |
+crypto | 암호화(Encryption) 확장 (AES, SHA 등) |
+dotprod | 점적(dot product) 명령어 |
+sm4 | SM4 암호화 명령어 |
AArch64 머신 지시자
.arch armv8-a+sve+lse
.arch_extension sve2
.no_arch_extension sve
.cpu cortex-a78
.global aarch64_func
.type aarch64_func, %function
aarch64_func:
.cfi_startproc
stp x29, x30, [sp, #-16]!
.cfi_def_cfa_offset 16
.cfi_offset 29, -16
.cfi_offset 30, -8
mov x29, sp
.cfi_def_cfa_register 29
ldp x29, x30, [sp], #16
.cfi_def_cfa rsp, 0
ret
.cfi_endproc
.size aarch64_func, .-aarch64_func
AArch64 주소 지정 방식
ldr x0, [x1]
ldr x0, [x1, #8]
ldr x0, [x1, x2]
ldr x0, [x1, x2, lsl #3]
ldr x0, [x1, #8]!
ldr x0, [x1], #8
adr x0, my_label
adrp x0, my_page
add x0, x0, :lo12:my_page
RISC-V 아키텍처 기능
GNU Assembler의 RISC-V(RV32/RV64) 지원 기능입니다.
RISC-V 전용 옵션
| 옵션 | 설명 |
|---|---|
-march=rv64gc | ISA 문자열. rv32/rv64 + 확장 (g=IMAFD, c=압축) |
-mabi=lp64d | ABI: ilp32, lp64, lp64d(하드 FP) 등 |
-mno-relax | 링커 릴랙세이션(instruction relaxation) 비활성화 |
-mcsr-check | CSR 읽기/쓰기 접근 권한 체크 |
RISC-V 머신 지시자
| 지시자 | 설명 |
|---|---|
.option rvc | 압축(C) 명령어 허용 |
.option norvc | 압축 명령어 금지 (정렬 보장) |
.option relax | 릴랙세이션 활성화 (기본) |
.option norelax | 릴랙세이션 비활성화 |
.option pic | PIC 코드 생성 모드 |
.option nopic | 비-PIC 모드 |
.option push | 현재 옵션 상태 저장 |
.option pop | 저장된 옵션 상태 복원 |
.attribute tag, value | ABI/아키텍처 속성 설정 (RISC-V ELF ABI) |
RISC-V 어셈블러 수정자
lui a0, %hi(symbol)
addi a0, a0, %lo(symbol)
auipc a0, %pcrel_hi(symbol)
addi a0, a0, %pcrel_lo(1b)
auipc a0, %got_pcrel_hi(symbol)
ld a0, %pcrel_lo(1b)(a0)
auipc a0, %tls_ie_pcrel_hi(tls_var)
ld a0, %pcrel_lo(1b)(a0)
.global riscv_func
.type riscv_func, @function
riscv_func:
.cfi_startproc
addi sp, sp, -16
.cfi_adjust_cfa_offset 16
sd ra, 8(sp)
.cfi_offset ra, -8
sd s0, 0(sp)
.cfi_offset s0, -16
addi s0, sp, 16
.cfi_def_cfa s0, 0
ld ra, 8(sp)
ld s0, 0(sp)
addi sp, sp, 16
.cfi_restore ra
.cfi_restore s0
.cfi_def_cfa sp, 0
ret
.cfi_endproc
.size riscv_func, .-riscv_func
커널에서 as 활용 실전
리눅스 커널은 as를 광범위하게 사용합니다. 커널 특유의 패턴과 매크로를 이해하면 커널 어셈블리 코드를 읽고 수정할 수 있습니다.
커널 심볼(Kernel Symbol) 매크로 (linkage.h)
SYM_FUNC_START(my_kernel_func)
movq %rdi, %rax
ret
SYM_FUNC_END(my_kernel_func)
SYM_FUNC_START_LOCAL(__helper)
ret
SYM_FUNC_END(__helper)
SYM_CODE_START(entry_SYSCALL_64)
.cfi_startproc simple
.cfi_signal_frame
.cfi_endproc
SYM_CODE_END(entry_SYSCALL_64)
SYM_DATA_START(my_kernel_data)
.long 0x12345678
SYM_DATA_END_LABEL(my_kernel_data, SYM_L_GLOBAL, my_kernel_data_end)
커널 섹션 활용 패턴
.section ".init.text", "ax"
.global kernel_init_func
kernel_init_func:
ret
SYM_FUNC_START(patched_func)
.cfi_startproc
1: nop
.pushsection .altinstructions, "a"
.long 1b - .
.long 2f - .
.byte X86_FEATURE_SOMETHING
.byte 1b_size
.byte 2f_size
.popsection
.pushsection .altinstr_replacement, "ax"
2: pause
.popsection
ret
.cfi_endproc
SYM_FUNC_END(patched_func)
SYM_FUNC_START(safe_copy)
.cfi_startproc
1: movq (%rsi), %rax
movq %rax, (%rdi)
ret
.cfi_endproc
.pushsection __ex_table, "a"
.quad 1b
.quad .Lfixup
.popsection
.Lfixup:
xorl %eax, %eax
ret
SYM_FUNC_END(safe_copy)
커널 빌드에서 as 활용
# Kbuild가 .S 파일을 빌드할 때 내부적으로:
# 1) cpp로 전처리 (CONFIG_* 매크로 치환)
# 2) as로 어셈블
# 수동으로 동일한 과정 재현:
# 전처리만
gcc -E -D__ASSEMBLY__ -Iinclude -o foo.i arch/x86/kernel/foo.S
# 어셈블 리스팅 생성 (디버깅 용도)
as -aln=foo.lst --64 -o foo.o foo.i
# 커널 크로스 컴파일
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- arch/arm64/kernel/entry.o
# 특정 오브젝트만 상세 어셈블리 확인
objdump -dS arch/x86/kernel/entry_64.o | less
자주 쓰는 커널 GAS 패턴
lock addl $1, (%rdi)
lock xaddl %eax, (%rdi)
lock cmpxchgl %ecx, (%rdi)
mfence
lfence
sfence
lock addl $0, (%rsp)
.Lwait:
pause
cmpb $0, (%rdi)
jne .Lwait
movq %gs:40, %rax
movq %rax, -8(%rbp)
GAS 표현식과 연산자
GNU Assembler는 어셈블 시간에 평가되는 산술·논리·비트 표현식을 지원합니다. 상수 계산, 주소 오프셋 산출, 조건부 어셈블리 등에 필수적입니다.
연산자 우선순위(Priority)
| 우선순위 | 연산자 | 설명 |
|---|---|---|
| 1 (높음) | - ~ ! | 단항: 부정, 비트 NOT, 논리 NOT |
| 2 | * / % | 곱셈, 나눗셈, 나머지 |
| 3 | + - | 덧셈, 뺄셈 |
| 4 | << >> | 좌·우 시프트 |
| 5 | & | 비트 AND |
| 6 | ^ | 비트 XOR |
| 7 | | | 비트 OR |
| 8 | == != < > <= >= | 비교 (결과: 0 또는 -1) |
| 9 | && | 논리 AND |
| 10 (낮음) | || | 논리 OR |
표현식 활용 예제
/* 상수 산술 */
.equ PAGE_SHIFT, 12
.equ PAGE_SIZE, (1 << PAGE_SHIFT) /* 4096 */
.equ PAGE_MASK, (~(PAGE_SIZE - 1)) /* 0xFFFFF000 */
/* 구조체 오프셋 계산 */
.equ TASK_STATE, 0
.equ TASK_FLAGS, 8
.equ TASK_STACK, 16
.equ TASK_PID, 24
.equ TASK_SIZE, TASK_PID + 4 /* 28 */
/* 현재 위치(.) 활용 — 문자열 길이 계산 */
msg: .ascii "Hello, kernel!\n"
.equ MSG_LEN, . - msg /* 15 */
/* 정렬 연산 — 주소를 PAGE_SIZE 경계로 올림 */
.equ ALIGNED_SIZE, (RAW_SIZE + PAGE_SIZE - 1) & PAGE_MASK
/* 비트 플래그 조합 */
.equ PTE_PRESENT, (1 << 0)
.equ PTE_WRITE, (1 << 1)
.equ PTE_USER, (1 << 2)
.equ PTE_RW_USER, PTE_PRESENT | PTE_WRITE | PTE_USER /* 0x7 */
.equ 상수를 사용하면 가독성과 유지보수성이 크게 향상됩니다. 커널에서는 asm-offsets.c를 통해 C 구조체(Struct) 오프셋을 자동 생성하여 어셈블리에서 사용합니다.
조건부 어셈블리와 표현식 결합
/* CONFIG 값에 따라 다른 코드 생성 */
.equ CONFIG_NR_CPUS, 256
.if CONFIG_NR_CPUS > 128
/* 대규모 시스템 — 2-level 탐색 */
shrq $7, %rdi
andl $0x7f, %esi
.else
/* 소규모 시스템 — 직접 인덱스 */
andl $0xff, %edi
.endif
/* .irp + .if 조합으로 선택적 레지스터 저장 */
.macro SAVE_CALLEE_REGS save_rbx=1, save_r12=1
.if \save_rbx
pushq %rbx
.cfi_adjust_cfa_offset 8
.cfi_rel_offset %rbx, 0
.endif
.if \save_r12
pushq %r12
.cfi_adjust_cfa_offset 8
.cfi_rel_offset %r12, 0
.endif
.endm
링커 스크립트와 GAS 연동
GAS가 생성한 ELF 오브젝트의 섹션은 링커 스크립트(Linker Script, .lds)에 의해 최종 배치가 결정됩니다. 커널에서는 vmlinux.lds.S가 모든 섹션의 VMA(가상 메모리(Virtual Memory) 주소)를 정의합니다.
커널 특수 섹션과 GAS
커널은 표준 ELF 섹션 외에 다수의 특수 섹션을 정의합니다. 어셈블리에서 이 섹션들에 데이터를 삽입하려면 .pushsection/.popsection 패턴을 사용합니다.
| 섹션 | 용도 | GAS에서 사용 예 |
|---|---|---|
.init.text | 부팅 시에만 실행, 이후 메모리 해제 | .section ".init.text", "ax" |
.init.data | 초기화 데이터, 부팅 후 해제 | .section ".init.data", "aw" |
.altinstructions | CPU 기능별 대체 명령어 테이블 | .pushsection .altinstructions, "a" |
__ex_table | 예외 처리 테이블 (유저 메모리 접근) | .pushsection __ex_table, "a" |
.fixup | 예외 발생 시 복구 코드 | .section .fixup, "ax" |
__bug_table | BUG()/BUG_ON() 위치 기록 | .pushsection __bug_table, "aw" |
.note.GNU-stack | 실행 가능 스택 비활성화 표시 | .section .note.GNU-stack, "", @progbits |
__jump_table | static key 점프 테이블 | .pushsection __jump_table, "aw" |
링커 심볼 참조
/* 링커 스크립트가 정의한 심볼을 어셈블리에서 참조 */
.extern _stext /* 커널 텍스트 시작 */
.extern _etext /* 커널 텍스트 끝 */
.extern __bss_start /* BSS 시작 */
.extern __bss_stop /* BSS 끝 */
/* BSS 영역 0 초기화 (부트 코드 예시) */
clear_bss:
leaq __bss_start(%rip), %rdi
leaq __bss_stop(%rip), %rcx
subq %rdi, %rcx
shrq $3, %rcx /* 바이트 → qword 수 */
xorl %eax, %eax
rep stosq
ret
/* .note.GNU-stack — 실행 가능 스택 비활성화 (보안) */
.section .note.GNU-stack, "", @progbits
.note.GNU-stack 섹션이 없으면 일부 링커가 스택을 실행 가능(executable)으로 표시합니다. 커널 어셈블리 파일에서도 이 섹션을 반드시 포함하여 스택 실행을 방지하세요. GCC가 C 코드를 어셈블할 때는 자동으로 추가하지만, 순수 .S 파일에서는 수동으로 추가해야 합니다.
GAS 출력 디버깅(Debugging)
어셈블러 출력을 검증하고 문제를 진단하는 도구와 기법을 설명합니다. objdump, readelf, nm 등 GNU Binutils 도구를 활용합니다.
objdump로 디스어셈블 확인
# 기본 디스어셈블
objdump -d foo.o
# 소스와 디스어셈블 인터리브 (-g 옵션으로 어셈블한 경우)
objdump -dS foo.o
# 재배치 정보 포함
objdump -dr foo.o
# 특정 섹션만 디스어셈블
objdump -d -j .text foo.o
objdump -d -j .init.text foo.o
# Intel 문법으로 출력
objdump -d -M intel foo.o
# 전체 섹션 헤더 확인
objdump -h foo.o
readelf로 ELF 구조 분석
# 섹션 헤더 (플래그, 크기, 오프셋)
readelf -S foo.o
# 심볼 테이블
readelf -s foo.o
# 재배치 엔트리
readelf -r foo.o
# CFI 언와인드 정보 (DWARF .eh_frame)
readelf --debug-dump=frames foo.o
# 모든 DWARF 정보
readelf --debug-dump=info foo.o
# 노트 섹션 (GNU-stack, 빌드 ID 등)
readelf -n foo.o
CFI 정보 검증
# .eh_frame 섹션의 CFI 엔트리를 사람이 읽을 수 있는 형태로 출력
readelf --debug-dump=frames-interp foo.o
# 출력 예시:
# DW_CFA_def_cfa: r7 (rsp) ofs 8
# DW_CFA_offset: r16 (rip) at cfa-8
# DW_CFA_def_cfa_offset: 16
# DW_CFA_offset: r6 (rbp) at cfa-16
# 커널 ORC 언와인드 테이블 확인 (커널 빌드 후)
scripts/orc_dump vmlinux | head -20
# GDB에서 CFI 기반 백트레이스 확인
gdb -batch -ex 'file foo.o' -ex 'disas my_func'
readelf --debug-dump=frames-interp로 각 명령어 주소에서의 CFA 규칙과 레지스터 복원 규칙을 확인하는 것입니다. 스택 프레임(Stack Frame)이 변경되는 모든 지점에서 CFI 상태가 정확하게 추적되는지 검증하세요.
어셈블 리스팅 분석
# 전체 리스팅 생성 (주소, 기계어, 소스 나란히)
as -aln=foo.lst -o foo.o foo.s
# 리스팅 파일 형식:
# 줄번호 주소 기계어 소스
# 1 0000 4889E5 movq %rsp, %rbp
# 2 0003 4883EC20 subq $32, %rsp
# 매크로 전개 확인 (-m 추가)
as -almn=foo.lst -o foo.o foo.s
# 심볼 테이블 포함 (-s 추가)
as -alns=foo.lst -o foo.o foo.s
흔한 실수와 해결책
GNU Assembler로 커널 코드를 작성할 때 자주 발생하는 오류와 해결 방법을 정리합니다.
.align 해석 차이
.align의 인자 해석은 아키텍처마다 다릅니다. 이 차이를 무시하면 정렬 버그가 발생하고, 성능 저하나 크래시로 이어질 수 있습니다.
/* 잘못된 예: 아키텍처별로 다르게 해석됨 */
.align 4
/* x86: 4바이트 정렬 (의도한 대로)
* ARM: 2^4 = 16바이트 정렬 (과도한 정렬)
* RISC-V: 2^4 = 16바이트 정렬 (과도한 정렬) */
/* 올바른 예: 아키텍처 독립적 지시자 사용 */
.balign 4 /* 항상 4바이트 정렬 */
.p2align 2 /* 항상 2^2 = 4바이트 정렬 */
/* 커널 권장: 매크로 사용 */
/* arch/x86/include/asm/linkage.h의 ALIGN 매크로 */
/* 내부적으로 .p2align으로 구현되어 이식성 보장 */
CFI 불일치
/* 잘못된 예: push 후 CFI 업데이트 누락 */
bad_func:
.cfi_startproc
pushq %rbx
/* .cfi_adjust_cfa_offset 8 누락! */
/* .cfi_rel_offset %rbx, 0 누락! */
call some_func
/* 이 시점에서 백트레이스 시 스택 언와인드 실패 */
popq %rbx
ret
.cfi_endproc
/* 올바른 예 */
good_func:
.cfi_startproc
pushq %rbx
.cfi_adjust_cfa_offset 8
.cfi_rel_offset %rbx, 0
call some_func
popq %rbx
.cfi_adjust_cfa_offset -8
.cfi_restore %rbx
ret
.cfi_endproc
.size 누락
/* 잘못된 예: .size 없이 함수 정의 */
.global my_func
.type my_func, @function
my_func:
ret
/* .size 누락 → nm/objdump에서 크기 0, 프로파일러 혼동 */
/* 올바른 예 */
.global my_func
.type my_func, @function
my_func:
ret
.size my_func, .-my_func
/* 커널에서는 SYM_FUNC_END()가 .size를 자동 설정 */
RIP 상대 주소 누락 (x86-64)
/* 잘못된 예: 절대 주소 사용 → 재배치 오류 (PIE/KASLR) */
movq my_var, %rax /* R_X86_64_32S 재배치 */
/* 올바른 예: RIP 상대 주소 사용 */
movq my_var(%rip), %rax /* R_X86_64_PC32 재배치 */
leaq my_var(%rip), %rsi /* 주소 계산 */
/* KASLR이 활성화된 커널에서 절대 주소는 반드시 실패합니다 */
symbol(%rip))을 사용해야 합니다. 부트 코드(.init.text)에서도 예외가 아닙니다. AArch64에서는 adrp/add 쌍을, RISC-V에서는 auipc/addi 쌍을 사용하세요.
피연산자 순서 혼동 (AT&T vs Intel)
/* AT&T 문법: 소스 → 목적 */
movq %rax, %rbx /* rbx = rax */
/* Intel 문법: 목적 ← 소스 */
.intel_syntax noprefix
mov rbx, rax /* rbx = rax (동일) */
.att_syntax prefix
/* 흔한 실수: AT&T에서 Intel 순서로 작성 */
movq %rbx, %rax /* 실수! rax = rbx (의도와 반대) */
매크로 인자 이스케이프
/* 잘못된 예: 매크로 인자를 \ 없이 사용 */
.macro PUSH_REG reg
pushq reg /* "reg" 심볼을 참조 — 원하는 동작이 아님 */
.endm
/* 올바른 예: 백슬래시로 매크로 인자 참조 */
.macro PUSH_REG reg
pushq \reg /* 매크로 인자 값으로 치환 */
.endm
/* 문자열 연결이 필요한 경우 */
.macro DEFINE_HANDLER name
.global handler_\name /* 연결 → handler_page_fault 등 */
handler_\name:
ret
.endm
DEFINE_HANDLER page_fault
DEFINE_HANDLER divide_error
asm-offsets 메커니즘
C 구조체의 필드 오프셋을 어셈블리에서 안전하게 사용하기 위해, 커널은 asm-offsets.c 메커니즘을 제공합니다. 컴파일 시 C 구조체 레이아웃을 분석하여 GAS 상수로 변환합니다.
작동 원리
/* arch/x86/kernel/asm-offsets.c */
#include <linux/sched.h>
#include <asm/thread_info.h>
void common(void)
{
/* DEFINE(상수명, 오프셋 표현식) */
OFFSET(TASK_THREAD_SP, task_struct, thread.sp);
OFFSET(TASK_THREAD_SP0, task_struct, thread.sp0);
OFFSET(TASK_FLAGS, task_struct, flags);
DEFINE(TASK_STRUCT_SIZE, sizeof(struct task_struct));
BLANK();
OFFSET(PT_REGS_RIP, pt_regs, ip);
OFFSET(PT_REGS_RSP, pt_regs, sp);
OFFSET(PT_REGS_RAX, pt_regs, ax);
}
생성된 헤더
/* include/generated/asm-offsets.h (자동 생성) */
#define TASK_THREAD_SP 2776 /* offsetof(struct task_struct, thread.sp) */
#define TASK_THREAD_SP0 2784 /* offsetof(struct task_struct, thread.sp0) */
#define TASK_FLAGS 44 /* offsetof(struct task_struct, flags) */
#define TASK_STRUCT_SIZE 9536 /* sizeof(struct task_struct) */
#define PT_REGS_RIP 128 /* offsetof(struct pt_regs, ip) */
#define PT_REGS_RSP 152 /* offsetof(struct pt_regs, sp) */
#define PT_REGS_RAX 80 /* offsetof(struct pt_regs, ax) */
어셈블리에서 사용
#include <asm/asm-offsets.h>
/* 현재 태스크의 커널 스택 포인터 로드 */
SYM_FUNC_START(switch_to_asm)
.cfi_startproc
/* 이전 태스크의 스택 포인터 저장 */
movq %rsp, TASK_THREAD_SP(%rdi) /* prev->thread.sp = rsp */
/* 새 태스크의 스택 포인터 복원 */
movq TASK_THREAD_SP(%rsi), %rsp /* rsp = next->thread.sp */
/* 새 태스크의 스택 기저 업데이트 */
movq TASK_THREAD_SP0(%rsi), %rdx
movq %rdx, PER_CPU_VAR(cpu_tss_rw + TSS_sp0)
ret
.cfi_endproc
SYM_FUNC_END(switch_to_asm)
CONFIG_* 옵션, 패딩 규칙에 따라 변합니다. 어셈블리에서 상수를 하드코딩하면 커널 업데이트 시 오프셋 불일치로 크래시가 발생합니다. asm-offsets.c는 빌드 시마다 올바른 오프셋을 재계산하여 이 문제를 근본적으로 방지합니다.
인라인 어셈블리와 GAS 연계
GCC 인라인 어셈블리(asm volatile(...))는 내부적으로 GAS 문법을 사용합니다. 순수 .S 파일과 인라인 어셈블리의 차이점과 상호 작용을 이해하면 커널 코드를 더 효과적으로 작성할 수 있습니다.
순수 .S 파일 vs 인라인 어셈블리 비교
| 항목 | 순수 .S 파일 | 인라인 어셈블리 (C 내장) |
|---|---|---|
| 전처리 | cpp로 별도 전처리 (#include, #define) | GCC가 C 전처리와 함께 처리 |
| 레지스터 할당 | 프로그래머가 직접 관리 | GCC 레지스터 할당기가 관리 (제약 조건으로 지정) |
| CFI 정보 | 수동으로 .cfi_* 지시자 삽입 | GCC가 자동 생성 (대부분의 경우) |
| 최적화 | 어셈블러가 최적화 안 함 | GCC가 주변 C 코드와 함께 최적화 |
| 심볼 관리 | .global, .type, .size 수동 설정 | GCC가 함수 레벨에서 자동 관리 |
| 이식성 | 아키텍처 종속적 | 제약 조건으로 일부 아키텍처 추상화 가능 |
| 디버깅 | .loc 지시자로 수동 위치 매핑(Mapping) | GCC가 자동으로 소스 위치 매핑 |
| 사용 시기 | 부트 코드, 인터럽트(Interrupt) 핸들러, context switch | 원자 연산, 특수 명령어, 성능 크리티컬 경로 |
인라인 어셈블리에서 GAS 문법 주의점
/* 인라인 어셈블리에서 % 이스케이프 */
static inline u64 read_cr3(void)
{
u64 val;
/* AT&T 문법에서 %%로 레지스터 접두사 이스케이프 */
asm volatile("movq %%cr3, %0" : "=r"(val));
return val;
}
/* 여러 명령어 — \n\t로 구분 */
static inline void invlpg(unsigned long addr)
{
asm volatile("invlpg (%0)" : : "r"(addr) : "memory");
}
/* lock 프리픽스 — GAS와 동일한 문법 */
static inline void atomic_inc(atomic_t *v)
{
asm volatile(
"lock incl %0"
: "+m"(v->counter)
: /* no input */
: "cc" /* 플래그 레지스터 수정 */
);
}
%0, %1 등은 GCC 피연산자 참조이고, %%rax처럼 %%가 실제 레지스터 접두사입니다. 순수 .S 파일에서는 %rax처럼 % 하나만 사용합니다. 이 차이를 혼동하면 어셈블 오류가 발생합니다. 자세한 인라인 어셈블리 문법은 어셈블리 종합 페이지를 참고하세요.
고급 매크로 기법
커널에서 사용하는 고급 GAS 매크로 패턴으로, 반복 코드 제거와 아키텍처 추상화에 활용됩니다.
재귀 매크로
/* n개 레지스터를 재귀적으로 push */
.macro PUSH_REGS first, rest:vararg
pushq \first
.cfi_adjust_cfa_offset 8
.cfi_rel_offset \first, 0
.ifnb \rest
PUSH_REGS \rest
.endif
.endm
/* 사용 */
PUSH_REGS %rbx, %r12, %r13, %r14, %r15
/* 5개의 pushq + CFI 지시자가 자동 생성 */
테이블 생성 매크로
/* 인터럽트 벡터 테이블 자동 생성 */
.macro INTENTRY num
.global int_entry_\num
int_entry_\num:
.if !(\num == 8 || (\num >= 10 && \num <= 14) || \num == 17 || \num == 21)
pushq $0 /* 더미 에러 코드 (HW가 안 넣는 경우) */
.endif
pushq $\num /* 벡터 번호 */
jmp common_interrupt
.endm
/* 0~255번 벡터 엔트리 자동 생성 */
vector=0
.rept 256
INTENTRY vector
vector=vector+1
.endr
/* 점프 테이블 (함수 포인터 배열) */
.section .rodata
.global int_entry_table
int_entry_table:
vector=0
.rept 256
.quad int_entry_0 + vector * (int_entry_1 - int_entry_0)
vector=vector+1
.endr
Alternative Instructions 매크로
/* CPU 기능에 따라 런타임에 명령어 교체 */
.macro ALTERNATIVE oldinstr, newinstr, feature
140:
\oldinstr
141:
.pushsection .altinstructions, "a"
.long 140b - . /* 원본 명령어 위치 */
.long 144f - . /* 대체 명령어 위치 */
.word \feature /* X86_FEATURE_* */
.byte 141b - 140b /* 원본 크기 */
.byte 145f - 144f /* 대체 크기 */
.popsection
.pushsection .altinstr_replacement, "ax"
144:
\newinstr
145:
.popsection
.endm
/* 사용 예: LFENCE를 지원하면 NOP 대신 LFENCE 사용 */
ALTERNATIVE "nop", "lfence", X86_FEATURE_LFENCE_RDTSC
ALTERNATIVE 매크로는 arch/x86/include/asm/alternative.h에 정의되어 있으며, 부팅 시 apply_alternatives()가 .altinstructions 테이블을 순회하며 CPU CPUID 결과에 따라 원본 명령어를 대체 명령어로 패치(Patch)합니다. 이를 통해 단일 커널 바이너리로 다양한 CPU를 최적 지원합니다. 자세한 내용은 CPUID 명령어 페이지를 참고하세요.
부트 코드 작성 패턴
커널 부트 코드는 GAS의 특수 기능을 집중적으로 활용합니다. 리얼 모드(16비트), 보호 모드(32비트), 롱 모드(64비트)를 순차적으로 전환하며, 각 모드에서 다른 코드 생성 규칙을 적용해야 합니다.
모드 전환 코드
/* 16비트 리얼 모드 → 32비트 보호 모드 전환 */
.code16 /* 16비트 코드 생성 */
start16:
cli
lgdt gdt_desc /* GDT 로드 */
/* PE 비트 설정하여 보호 모드 진입 */
movl %cr0, %eax
orl $1, %eax
movl %eax, %cr0
/* 32비트 코드 세그먼트로 far jump */
ljmpl $0x08, $start32
.code32 /* 32비트 코드 생성 */
start32:
movw $0x10, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
/* 64비트 롱 모드로 전환 준비 (페이지 테이블 설정 등) */
/* ... */
.code64 /* 64비트 코드 생성 */
start64:
movq $STACK_TOP, %rsp
call x86_64_start_kernel
부트 데이터 구조 정의
/* GDT (Global Descriptor Table) 정의 */
.section .rodata
.balign 16
gdt:
.quad 0x0000000000000000 /* 널 디스크립터 */
.quad 0x00cf9a000000ffff /* 32비트 코드 세그먼트 */
.quad 0x00cf92000000ffff /* 32비트 데이터 세그먼트 */
.quad 0x00209a0000000000 /* 64비트 코드 세그먼트 */
.quad 0x0000920000000000 /* 64비트 데이터 세그먼트 */
gdt_end:
gdt_desc:
.word gdt_end - gdt - 1 /* GDT 크기 - 1 */
.long gdt /* GDT 기저 주소 */
/* 초기 페이지 테이블 (Identity Mapping) */
.section .init.data
.balign 4096 /* 페이지 정렬 필수 */
init_pgt:
.quad init_pgt + 0x1000 + 0x67 /* PML4E → PDPT (Present|RW|User|Accessed|Dirty) */
.fill 511, 8, 0 /* 나머지 PML4E 비움 */
.code16, .code32, .code64 지시자를 잘못 배치하면 CPU가 엉뚱한 크기의 명령어를 디코딩합니다. 예를 들어 32비트 보호 모드에서 .code16 블록의 코드를 실행하면 즉시 크래시합니다. 모드 전환 직전/직후에 반드시 올바른 .code 지시자를 배치하세요.
GNU as 어셈블러 파이프라인
GNU Assembler는 단순한 1:1 변환기가 아니라 여러 단계로 구성된 파이프라인(Pipeline)을 통해 소스를 처리합니다. 내부적으로 프론트엔드(Front-end)와 백엔드(Back-end)가 분리되어 다양한 아키텍처를 지원합니다.
파이프라인 단계 상세
| 단계 | 입력 | 출력 | 핵심 동작 |
|---|---|---|---|
| 전처리 | .S 파일 | .s 파일 | C 전처리기(cpp)가 #include, #define, #ifdef 처리. GCC가 -D__ASSEMBLY__를 자동 정의 |
| 토큰화 | 소스 텍스트 | 토큰 스트림 | 줄 분리, 레이블·지시자·명령어·피연산자 식별, 주석 제거 |
| 파싱 | 토큰 스트림 | 내부 표현 | 표현식 평가, 섹션 전환, 심볼 정의, 데이터 삽입 |
| 매크로 확장 | 매크로 호출 | 확장된 코드 | .macro/.rept/.irp 확장, 재귀 매크로 처리 |
| 명령어 인코딩 | 니모닉(Mnemonic) + 피연산자 | 기계어 바이트 | 아키텍처 백엔드가 opcode 테이블 참조하여 인코딩 |
| 릴랙세이션(Relaxation) | 분기/점프 명령어 | 최적 크기 인코딩 | short jump ↔ near jump 자동 선택, RISC-V 링커 릴랙세이션 |
| 재배치 생성 | 미해결 심볼 참조 | 재배치 엔트리 | 링커가 채울 주소 슬롯과 재배치 타입 기록 |
| ELF 출력 | 모든 데이터 | .o 파일 | 섹션 헤더, 심볼 테이블, 재배치 테이블, DWARF 정보를 ELF로 직렬화 |
멀티패스 어셈블리와 전방 참조
GNU Assembler는 전방 참조(Forward Reference)를 지원하기 위해 최소 2패스를 수행합니다. 첫 번째 패스에서 심볼 위치를 추정하고, 두 번째 패스에서 확정합니다.
/* 전방 참조 예시 — jmp 타겟이 아래에 정의됨 */
jmp target_label /* 패스 1: 크기 추정 (short? near?) */
/* ... 중간 코드 ... */
target_label: /* 패스 2: 실제 거리 계산 → 인코딩 확정 */
ret
/* 릴랙세이션: 거리에 따라 자동으로 인코딩 선택 */
/* ±127바이트 → EB xx (2바이트 short jump) */
/* ±2GB → E9 xx xx xx xx (5바이트 near jump) */
x86-64 어셈블리
x86-64 아키텍처의 레지스터 체계, 주소 지정 모드, 호출 규약(Calling Convention)을 상세히 다룹니다.
x86-64 범용 레지스터
| 64비트 | 32비트 | 16비트 | 8비트(하) | 8비트(상) | System V ABI 역할 | Callee 저장 |
|---|---|---|---|---|---|---|
%rax | %eax | %ax | %al | %ah | 반환값 | |
%rbx | %ebx | %bx | %bl | %bh | 범용 | O |
%rcx | %ecx | %cx | %cl | %ch | 4번째 인자 | |
%rdx | %edx | %dx | %dl | %dh | 3번째 인자 | |
%rsi | %esi | %si | %sil | - | 2번째 인자 | |
%rdi | %edi | %di | %dil | - | 1번째 인자 | |
%rbp | %ebp | %bp | %bpl | - | 프레임 포인터 | O |
%rsp | %esp | %sp | %spl | - | 스택 포인터 | O |
%r8 | %r8d | %r8w | %r8b | - | 5번째 인자 | |
%r9 | %r9d | %r9w | %r9b | - | 6번째 인자 | |
%r10 | %r10d | %r10w | %r10b | - | static chain | |
%r11 | %r11d | %r11w | %r11b | - | 임시 | |
%r12~%r15 | %r12d~%r15d | %r12w~%r15w | %r12b~%r15b | - | 범용 | O |
x86-64 주소 지정 모드 종합
/* 즉시값 (Immediate) */
movq $42, %rax /* rax = 42 */
movq $0xFFFF_FFFF_FFFF_FFFF, %rax /* 64비트 즉시값 (movabs) */
/* 레지스터 직접 */
movq %rax, %rbx /* rbx = rax */
/* 메모리 직접 (절대 주소 — 커널에서 비권장) */
movq 0x1000, %rax /* rax = *(0x1000) */
/* 레지스터 간접 */
movq (%rax), %rbx /* rbx = *rax */
/* 베이스 + 변위 */
movq 16(%rbp), %rax /* rax = *(rbp + 16) */
movq -8(%rbp), %rax /* rax = *(rbp - 8) */
/* 베이스 + 인덱스 */
movq (%rax, %rcx), %rdx /* rdx = *(rax + rcx) */
/* 베이스 + 인덱스 * 스케일 + 변위 (SIB 전체) */
movq 8(%rax, %rcx, 8), %rdx /* rdx = *(rax + rcx*8 + 8) */
/* 스케일: 1, 2, 4, 8만 허용 */
/* RIP 상대 주소 (x86-64 기본, KASLR 필수) */
leaq my_data(%rip), %rsi /* rsi = &my_data (PC-relative) */
movq my_var(%rip), %rax /* rax = my_var (PC-relative load) */
/* 세그먼트 오버라이드 (per-CPU 변수 접근) */
movq %gs:0x28, %rax /* 스택 카나리 (커널) */
movq %gs:current_task, %rdi /* per-CPU current task */
시스템 콜 진입점 (x86-64)
리눅스 커널의 entry_SYSCALL_64는 x86-64에서 가장 중요한 어셈블리 코드 중 하나입니다. SYSCALL 명령어로 유저 모드에서 커널 모드로 전환될 때 실행됩니다.
/* arch/x86/entry/entry_64.S — 핵심 구조 (간략화) */
SYM_CODE_START(entry_SYSCALL_64)
.cfi_startproc simple
.cfi_signal_frame
/* SYSCALL 진입 시 CPU 상태:
* RCX = 유저 RIP (반환 주소)
* R11 = 유저 RFLAGS
* RAX = 시스템 콜 번호
* RDI, RSI, RDX, R10, R8, R9 = 인자 1~6 */
/* 커널 스택으로 전환 */
swapgs /* GS 베이스를 커널 per-CPU로 전환 */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
/* pt_regs 프레임 구축 */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
/* 범용 레지스터 저장 */
pushq %rax /* pt_regs->orig_ax (시스템 콜 번호) */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
/* 시스템 콜 디스패치 */
movq %rax, %rdi
call do_syscall_64
/* 반환 경로 ... */
.cfi_endproc
SYM_CODE_END(entry_SYSCALL_64)
ARM64 어셈블리
AArch64(ARM64) 아키텍처의 A64 명령어 세트, 레지스터 규약, 조건 실행 메커니즘을 상세히 설명합니다.
AArch64 레지스터 체계
| 레지스터 | 크기 | AAPCS64 역할 | Callee 저장 |
|---|---|---|---|
x0~x7 | 64비트 | 인자 / 반환값 (x0~x1 반환) | |
x8 | 64비트 | 간접 반환 레지스터 (구조체 반환 주소) | |
x9~x15 | 64비트 | 임시 (caller-saved) | |
x16 (IP0) | 64비트 | 인트라프로시저 콜 스크래치 | |
x17 (IP1) | 64비트 | 인트라프로시저 콜 스크래치 | |
x18 | 64비트 | 플랫폼 레지스터 (커널: shadow call stack) | |
x19~x28 | 64비트 | 범용 (callee-saved) | O |
x29 (FP) | 64비트 | 프레임 포인터 | O |
x30 (LR) | 64비트 | 링크 레지스터 (반환 주소) | O |
SP | 64비트 | 스택 포인터 (16바이트 정렬 필수) | O |
XZR/WZR | 64/32비트 | 제로 레지스터 (읽으면 0, 쓰면 버림) | - |
A64 핵심 명령어 카테고리
/* === 데이터 처리 (레지스터) === */
add x0, x1, x2 /* x0 = x1 + x2 */
sub x0, x1, x2, lsl #3 /* x0 = x1 - (x2 << 3) */
adds x0, x1, x2 /* 더하기 + 플래그 업데이트 */
madd x0, x1, x2, x3 /* x0 = x3 + x1*x2 */
udiv x0, x1, x2 /* x0 = x1 / x2 (부호 없는) */
/* === 논리 연산 === */
and x0, x1, #0xFF /* x0 = x1 & 0xFF */
orr x0, xzr, x1 /* x0 = x1 (MOV 의사명령어) */
eor x0, x1, x2 /* x0 = x1 ^ x2 */
bic x0, x1, x2 /* x0 = x1 & ~x2 (bit clear) */
/* === 메모리 접근 === */
ldr x0, [x1] /* 64비트 로드 */
ldp x0, x1, [sp] /* 페어 로드 (128비트) */
str x0, [x1, #8] /* 오프셋 스토어 */
stp x29, x30, [sp, #-16]! /* 프리-인덱스 페어 스토어 */
ldar x0, [x1] /* acquire 로드 (메모리 순서) */
stlr x0, [x1] /* release 스토어 (메모리 순서) */
/* === 분기 === */
b label /* 무조건 분기 */
bl func /* 분기 + 링크 (함수 호출) */
br x16 /* 레지스터 간접 분기 */
blr x16 /* 레지스터 간접 호출 */
ret /* x30으로 반환 */
cbz x0, label /* x0 == 0이면 분기 */
cbnz x0, label /* x0 != 0이면 분기 */
tbz x0, #5, label /* x0 비트5 == 0이면 분기 */
/* === 조건 선택 (분기 없는 조건부 실행) === */
cmp x0, x1
csel x2, x3, x4, eq /* x2 = (x0==x1) ? x3 : x4 */
csinc x2, x3, x3, ne /* x2 = (x0!=x1) ? x3 : x3+1 */
cset x2, eq /* x2 = (x0==x1) ? 1 : 0 */
/* === 시스템 명령어 === */
mrs x0, CurrentEL /* 시스템 레지스터 읽기 */
msr DAIF, x0 /* 시스템 레지스터 쓰기 */
svc #0 /* 시스템 콜 (EL0→EL1) */
hvc #0 /* 하이퍼바이저 콜 (EL1→EL2) */
smc #0 /* 보안 모니터 콜 (EL1→EL3) */
eret /* 예외 반환 */
dmb ish /* 데이터 메모리 배리어 */
dsb ish /* 데이터 동기화 배리어 */
isb /* 명령어 동기화 배리어 */
ARM64 커널 예외 진입점
/* arch/arm64/kernel/entry.S — 예외 벡터 테이블 (간략화) */
.section ".entry.text", "ax"
/* 예외 벡터 테이블은 2048바이트 정렬, 각 엔트리 128바이트 */
.balign 2048
SYM_CODE_START(vectors)
/* EL1t: 현재 EL, SP_EL0 사용 */
kernel_ventry el1_sync_invalid
kernel_ventry el1_irq_invalid
kernel_ventry el1_fiq_invalid
kernel_ventry el1_error_invalid
/* EL1h: 현재 EL, SP_EL1 사용 (일반적) */
kernel_ventry el1h_64_sync
kernel_ventry el1h_64_irq
kernel_ventry el1h_64_fiq
kernel_ventry el1h_64_error
/* EL0_64: 유저스페이스 64비트 */
kernel_ventry el0t_64_sync
kernel_ventry el0t_64_irq
kernel_ventry el0t_64_fiq
kernel_ventry el0t_64_error
/* EL0_32: 유저스페이스 32비트 (compat) */
kernel_ventry el0t_32_sync
kernel_ventry el0t_32_irq
kernel_ventry el0t_32_fiq
kernel_ventry el0t_32_error
SYM_CODE_END(vectors)
/* 각 kernel_ventry 매크로는 128바이트 슬롯 내에서:
* 1. 커널 스택으로 전환
* 2. 레지스터 저장 (x0~x30, SP_EL0, ELR_EL1, SPSR_EL1)
* 3. 핸들러 함수 호출 */
RISC-V 어셈블리
RISC-V의 RV64I 기본 명령어(Base Integer Instructions), 확장(M/A/F/D), 의사명령어(Pseudo-instructions)를 상세히 다룹니다.
RISC-V 레지스터 규약 (RV64)
| 레지스터 | ABI 이름 | 용도 | Callee 저장 |
|---|---|---|---|
x0 | zero | 하드와이어드 0 | - |
x1 | ra | 반환 주소 | |
x2 | sp | 스택 포인터 | O |
x3 | gp | 글로벌 포인터 | - |
x4 | tp | 스레드 포인터 | - |
x5~x7 | t0~t2 | 임시 | |
x8 | s0/fp | 프레임 포인터 / callee-saved | O |
x9 | s1 | callee-saved | O |
x10~x11 | a0~a1 | 인자 / 반환값 | |
x12~x17 | a2~a7 | 인자 | |
x18~x27 | s2~s11 | callee-saved | O |
x28~x31 | t3~t6 | 임시 |
RV64I 기본 명령어
/* === 산술 연산 === */
add a0, a1, a2 /* a0 = a1 + a2 */
addi a0, a1, 42 /* a0 = a1 + 42 (즉시값 ±2048) */
sub a0, a1, a2 /* a0 = a1 - a2 */
lui a0, 0x12345 /* a0 = 0x12345 << 12 (상위 20비트 로드) */
auipc a0, 0x12345 /* a0 = PC + (0x12345 << 12) */
/* === 논리 연산 === */
and a0, a1, a2 /* a0 = a1 & a2 */
or a0, a1, a2 /* a0 = a1 | a2 */
xor a0, a1, a2 /* a0 = a1 ^ a2 */
sll a0, a1, a2 /* a0 = a1 << a2 (논리 좌측) */
srl a0, a1, a2 /* a0 = a1 >> a2 (논리 우측) */
sra a0, a1, a2 /* a0 = a1 >> a2 (산술 우측) */
/* === 비교 === */
slt a0, a1, a2 /* a0 = (a1 < a2) ? 1 : 0 (부호 있는) */
sltu a0, a1, a2 /* 부호 없는 비교 */
/* === 메모리 접근 === */
ld a0, 0(a1) /* 64비트 로드: a0 = *(a1 + 0) */
lw a0, 4(a1) /* 32비트 로드 (부호 확장) */
lbu a0, 0(a1) /* 8비트 로드 (부호 없는) */
sd a0, 0(a1) /* 64비트 스토어 */
sw a0, 4(a1) /* 32비트 스토어 */
/* === 분기 === */
beq a0, a1, label /* a0 == a1이면 분기 */
bne a0, a1, label /* a0 != a1이면 분기 */
blt a0, a1, label /* a0 < a1이면 분기 (부호 있는) */
bge a0, a1, label /* a0 >= a1이면 분기 (부호 있는) */
jal ra, func /* 함수 호출 (ra = PC+4, PC = func) */
jalr ra, 0(a0) /* 간접 호출 */
/* === M 확장 (곱셈/나눗셈) === */
mul a0, a1, a2 /* a0 = a1 * a2 (하위 64비트) */
mulh a0, a1, a2 /* a0 = (a1 * a2) >> 64 (상위 64비트) */
div a0, a1, a2 /* a0 = a1 / a2 */
rem a0, a1, a2 /* a0 = a1 % a2 */
/* === A 확장 (원자 연산) === */
lr.d a0, (a1) /* Load-Reserved (64비트) */
sc.d a0, a2, (a1) /* Store-Conditional (성공: a0=0) */
amoswap.d a0, a2, (a1) /* 원자 스왑 */
amoadd.d a0, a2, (a1) /* 원자 덧셈 */
RISC-V 의사명령어
| 의사명령어 | 실제 변환 | 설명 |
|---|---|---|
nop | addi x0, x0, 0 | 아무 동작 안 함 |
li rd, imm | lui + addi (조합) | 즉시값 로드 (임의 크기) |
la rd, symbol | auipc + addi | 주소 로드 (PC-relative) |
mv rd, rs | addi rd, rs, 0 | 레지스터 이동 |
not rd, rs | xori rd, rs, -1 | 비트 NOT |
neg rd, rs | sub rd, x0, rs | 부정 |
j offset | jal x0, offset | 무조건 점프 |
call symbol | auipc ra, ...; jalr ra, ... | 함수 호출 (원거리) |
ret | jalr x0, 0(ra) | 함수 반환 |
beqz rs, off | beq rs, x0, off | 0이면 분기 |
bnez rs, off | bne rs, x0, off | 0이 아니면 분기 |
seqz rd, rs | sltiu rd, rs, 1 | 0이면 1 설정 |
fence | fence iorw, iorw | 전체 메모리 펜스 |
인라인 어셈블리 상세
GCC 확장 asm의 제약 조건(Constraint), 클로버(Clobber) 리스트, 명명 피연산자(Named Operand) 등 고급 기법을 다룹니다.
제약 조건 참조 (x86-64)
| 제약 | 의미 | 매핑 대상 |
|---|---|---|
r | 범용 레지스터 | RAX~R15 중 아무거나 |
a | RAX/EAX/AX/AL | %rax 계열 |
b | RBX/EBX/BX/BL | %rbx 계열 |
c | RCX/ECX/CX/CL | %rcx 계열 |
d | RDX/EDX/DX/DL | %rdx 계열 |
S | RSI/ESI | %rsi 계열 |
D | RDI/EDI | %rdi 계열 |
m | 메모리 피연산자 | 유효 주소 |
i | 즉시 정수 상수 | 컴파일 타임 상수 |
n | 즉시 정수 (known value) | 어셈블 타임 상수 |
g | 범용 (r, m, i 중 선택) | GCC가 최적 선택 |
= | 쓰기 전용 출력 | 출력 피연산자 접두사 |
+ | 읽기/쓰기 입출력 | 입출력 피연산자 접두사 |
& | 얼리클로버 (early clobber) | 입력 읽기 전에 덮어쓸 수 있음 |
고급 인라인 어셈블리 예제
/* 명명 피연산자 (Named Operands) — 가독성 향상 */
static inline u64 rdmsr(u32 msr)
{
u32 lo, hi;
asm volatile(
"rdmsr"
: [low] "=a"(lo), [high] "=d"(hi)
: [index] "c"(msr)
);
return ((u64)hi << 32) | lo;
}
/* 얼리클로버 (&) — 출력이 입력 읽기 전에 덮어써질 때 */
static inline long cmpxchg_local(volatile long *ptr, long old, long new)
{
long prev;
asm volatile(
"lock cmpxchgq %[new], %[ptr]"
: [prev] "=&a"(prev), [ptr] "+m"(*ptr)
: [new] "r"(new), "0"(old)
: "memory", "cc"
);
return prev;
}
/* goto 레이블 — asm goto (GCC 4.5+, 커널 static key 기반) */
static __always_inline bool arch_static_branch(struct static_key *key, bool branch)
{
asm volatile goto(
"1: jmp %l[l_yes]\n\t"
".pushsection __jump_table, \"aw\"\n\t"
".balign 8\n\t"
".quad 1b, %l[l_yes], %c0\n\t"
".popsection\n\t"
: : "i"(key) : : l_yes
);
return false;
l_yes:
return true;
}
클로버 리스트 상세
| 클로버 | 의미 | 사용 시기 |
|---|---|---|
"cc" | 조건 코드(플래그) 레지스터 수정됨 | CMP, ADD, SUB 등 플래그 변경 명령어 |
"memory" | 메모리가 수정될 수 있음 (컴파일러 배리어) | 메모리 접근 명령어, 배리어 |
"%rax" | 해당 레지스터가 파괴됨 | 명시적으로 사용하는 레지스터가 출력에 없을 때 |
asm volatile은 컴파일러가 어셈블리 블록을 최적화(제거, 이동)하지 못하게 합니다. 부수 효과(side effect)가 있는 어셈블리(I/O, 원자 연산, 배리어)에는 반드시 volatile을 사용하세요. 순수 계산(입력→출력만)이면 volatile 없이도 됩니다.
링커 스크립트 연동
커널 링커 스크립트 vmlinux.lds.S의 주요 구조와 GAS 어셈블리에서 링커 심볼을 활용하는 패턴을 다룹니다.
vmlinux.lds.S 핵심 구조
/* arch/x86/kernel/vmlinux.lds.S — 핵심 섹션 배치 */
OUTPUT_FORMAT("elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(startup_64)
PHDRS {
text PT_LOAD FLAGS(5); /* r-x: 읽기+실행 */
data PT_LOAD FLAGS(6); /* rw-: 읽기+쓰기 */
init PT_LOAD FLAGS(7); /* rwx: 초기화 후 해제 */
note PT_NOTE FLAGS(0); /* 메타데이터 */
}
SECTIONS {
. = __START_KERNEL; /* 0xFFFFFFFF81000000 (기본) */
/* 텍스트 세그먼트 */
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
_stext = .;
HEAD_TEXT /* 부트 코드 */
TEXT_TEXT /* 일반 코드 */
SCHED_TEXT /* 스케줄러 코드 */
LOCK_TEXT /* 락 코드 */
KPROBES_TEXT /* kprobe 가능 코드 */
SOFTIRQENTRY_TEXT /* softirq 엔트리 */
ENTRY_TEXT /* 엔트리 코드 */
_etext = .;
} :text
/* 초기화 섹션 (부팅 후 해제) */
.init.text : {
_sinittext = .;
INIT_TEXT /* __init 함수들 */
_einittext = .;
} :init
/* 읽기 전용 데이터 */
.rodata : {
__start_rodata = .;
*(.rodata .rodata.*)
__end_rodata = .;
} :text
/* per-CPU 데이터 */
. = ALIGN(PAGE_SIZE);
.data..percpu : {
__per_cpu_start = .;
*(.data..percpu .data..percpu.*)
__per_cpu_end = .;
}
/* 데이터 세그먼트 */
.data : {
_sdata = .;
DATA_DATA
_edata = .;
} :data
/* BSS */
.bss : {
__bss_start = .;
*(.bss .bss.*)
__bss_stop = .;
}
_end = .;
}
어셈블리에서 링커 심볼 활용 패턴
/* 커널 부트 코드에서 링커 심볼 사용 */
.section ".head.text", "ax"
SYM_CODE_START(startup_64)
/* 물리-가상 오프셋 계산 */
leaq _text(%rip), %rbp
movq $_text, %rbx
subq %rbx, %rbp /* rbp = 물리-가상 오프셋 */
/* 커널 크기 계산 */
leaq _end(%rip), %rcx
leaq _text(%rip), %rdx
subq %rdx, %rcx /* rcx = 커널 이미지 크기 */
/* per-CPU 영역 범위 확인 */
leaq __per_cpu_start(%rip), %rdi
leaq __per_cpu_end(%rip), %rsi
SYM_CODE_END(startup_64)
DWARF 디버깅 정보
CFI 지시자가 생성하는 DWARF 정보의 구조와 커널 ORC 언와인더(Unwinder)와의 관계를 설명합니다.
.eh_frame 섹션 구조
CFI 지시자는 .eh_frame 섹션에 CIE(Common Information Entry)와 FDE(Frame Description Entry)를 생성합니다. 디버거와 예외 처리 런타임이 이 정보를 읽어 스택을 언와인드합니다.
| 구조 | 역할 | 내용 |
|---|---|---|
| CIE | 공통 초기 규칙 | 데이터 정렬 팩터(Data Alignment Factor), 코드 정렬 팩터, 반환 주소 레지스터, 초기 CFA 규칙 |
| FDE | 함수별 프레임 정보 | PC 범위, 해당 CIE 참조, 각 명령어 주소별 CFA 변화 기록 |
| CFA 연산 | 프레임 주소 계산 | DW_CFA_def_cfa, DW_CFA_offset, DW_CFA_advance_loc 등 바이트코드 |
# .eh_frame 디코딩 — 각 함수의 CFA 규칙 확인
readelf --debug-dump=frames-interp foo.o
# 출력 예시 (x86-64):
# CIE: CFA=rsp+8, ra=rip
# FDE: PC=0x0..0x20
# LOC CFA rbp ra
# 0x00 rsp+8 u c-8
# 0x01 rsp+16 c-16 c-8 ← pushq %rbp 이후
# 0x04 rbp+16 c-16 c-8 ← movq %rsp,%rbp 이후
# 0x1e rsp+8 u c-8 ← leave 이후
커널 ORC 언와인더
커널은 DWARF .eh_frame 대신 자체 ORC(Oops Rewind Capability) 형식을 사용합니다. ORC는 DWARF보다 간결하고 파싱이 빠르며, objtool이 오브젝트 파일을 분석하여 자동 생성합니다.
# ORC 언와인드 테이블 확인
scripts/orc_dump vmlinux | head -30
# 출력 예시:
# .text+0x200: sp:sp+8 bp:(und) type:call end:0
# .text+0x201: sp:sp+16 bp:prev type:call end:0
# .text+0x204: sp:bp+16 bp:prev type:call end:0
# objtool로 CFI 정합성 검증
objtool check --orc foo.o
# 관련 커널 설정
# CONFIG_UNWINDER_ORC=y ← 기본 (x86-64)
# CONFIG_UNWINDER_FRAME_POINTER=y ← 대안 (모든 아키텍처)
# CONFIG_STACK_VALIDATION=y ← objtool 검증 활성화
커널 어셈블리 실전 패턴
커널 소스에서 반복적으로 나타나는 어셈블리 패턴을 아키텍처별로 정리합니다. 이 패턴들을 이해하면 커널 코드를 읽고 수정하는 데 큰 도움이 됩니다.
컨텍스트 전환 패턴
/* x86-64 컨텍스트 전환 (arch/x86/kernel/process_64.S 간략화) */
SYM_FUNC_START(__switch_to_asm)
.cfi_startproc
/* callee-saved 레지스터 저장 */
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
/* 이전 태스크의 스택 포인터 저장 */
movq %rsp, TASK_THREAD_SP(%rdi) /* prev->thread.sp = rsp */
/* 새 태스크의 스택 포인터 복원 */
movq TASK_THREAD_SP(%rsi), %rsp /* rsp = next->thread.sp */
/* 스택 카나리 업데이트 */
#ifdef CONFIG_STACKPROTECTOR
movq TASK_STACK_CANARY(%rsi), %rbx
movq %rbx, PER_CPU_VAR(fixed_percpu_data) + FIXED_stack_canary
#endif
/* callee-saved 레지스터 복원 (새 태스크의 값) */
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
/* ret → 새 태스크의 반환 주소로 점프 */
ret
.cfi_endproc
SYM_FUNC_END(__switch_to_asm)
인터럽트 핸들러 패턴
/* x86-64 인터럽트 엔트리 패턴 (간략화) */
.macro idtentry_body cfunc has_error_code:req
/* 모든 범용 레지스터를 pt_regs 프레임에 저장 */
pushq %rax
pushq %rcx
pushq %rdx
pushq %rsi
pushq %rdi
pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %rbx
pushq %rbp
pushq %r12
pushq %r13
pushq %r14
pushq %r15
/* pt_regs 포인터를 첫 번째 인자로 전달 */
movq %rsp, %rdi
/* C 핸들러 호출 */
call \cfunc
/* 레지스터 복원 (역순) */
popq %r15
popq %r14
/* ... 나머지 레지스터 ... */
popq %rax
/* 에러 코드 제거 (있는 경우) */
.if \has_error_code
addq $8, %rsp
.endif
iretq /* 인터럽트 복귀 */
.endm
per-CPU 변수 접근 패턴
/* x86-64: GS 세그먼트 기반 per-CPU 접근 */
movq %gs:cpu_current_top_of_stack, %rsp /* 현재 CPU의 스택 top */
incl %gs:irq_count /* 현재 CPU의 IRQ 카운터++ */
/* ARM64: TPIDR_EL1 레지스터 기반 per-CPU 접근 */
mrs x0, tpidr_el1 /* per-CPU 베이스 주소 로드 */
ldr x1, [x0, #OFFSET] /* per-CPU 변수 읽기 */
/* RISC-V: GP 레지스터 또는 TP 레지스터 기반 */
ld a0, pcpu_offset(tp) /* 스레드 포인터 기반 per-CPU 접근 */
스핀락 어셈블리 패턴
/* x86-64 ticket spinlock (간략화) */
SYM_FUNC_START(__raw_spin_lock)
.cfi_startproc
movl $0x00010000, %eax /* inc head by 1 */
lock xaddl %eax, (%rdi) /* 원자적 fetch-and-add */
movzwl %ax, %ecx /* ecx = tail (내 티켓 번호) */
shrl $16, %eax /* eax = head (현재 서비스 번호) */
cmpl %eax, %ecx
je .Lgot_lock /* head == tail → 즉시 획득 */
.Lspin_wait:
pause /* CPU 힌트: 스핀 루프 최적화 */
movzwl (%rdi), %eax /* head 재로드 */
cmpl %eax, %ecx
jne .Lspin_wait /* 아직 내 차례 아님 */
.Lgot_lock:
ret
.cfi_endproc
SYM_FUNC_END(__raw_spin_lock)
/* ARM64 스핀락 (WFE 기반) */
1: ldaxr w1, [x0] /* load-acquire exclusive */
cbnz w1, 2f /* 이미 잠겨있으면 대기 */
stxr w2, w3, [x0] /* store exclusive */
cbnz w2, 1b /* 실패하면 재시도 */
ret
2: wfe /* Wait For Event (전력 절약) */
b 1b
아키텍처별 동일 패턴 비교
| 패턴 | x86-64 | ARM64 | RISC-V |
|---|---|---|---|
| 원자적 증가 | lock incl (%rdi) | ldxr w0,[x1]; add w0,w0,#1; stxr w2,w0,[x1] | amoadd.w zero,a0,(a1) |
| 메모리 배리어 | mfence | dmb ish | fence rw,rw |
| 시스템 콜 | syscall | svc #0 | ecall |
| 인터럽트 비활성 | cli | msr DAIFSet, #2 | csrc sstatus, 2 |
| NOP | nop (0x90) | nop (d503201f) | nop (addi x0,x0,0) |
| 함수 호출 | call func | bl func | jal ra, func |
| 함수 반환 | ret | ret (br x30) | ret (jalr x0,0(ra)) |
| 스택 push | pushq %rax | stp x0,x1,[sp,#-16]! | addi sp,sp,-8; sd a0,0(sp) |
| TLB 무효화 | invlpg (%rdi) | tlbi vale1is, x0 | sfence.vma x0, x0 |
| 캐시 플러시 | clflush (%rdi) | dc civac, x0 | cbo.flush (a0) |
매크로와 조건부 어셈블리
커널에서 사용하는 고급 매크로 패턴과 조건부 어셈블리 기법을 추가로 다룹니다.
SYM_* 매크로 체계
Linux 5.5부터 도입된 SYM_* 매크로는 어셈블리 심볼의 타입, 가시성, CFI 정보를 일관되게 관리합니다. 이전의 ENTRY()/END() 매크로를 대체합니다.
| 매크로 | 심볼 타입 | 가시성 | 용도 |
|---|---|---|---|
SYM_FUNC_START(name) | STT_FUNC | GLOBAL | 일반 함수 시작 (CFI 자동 포함) |
SYM_FUNC_START_LOCAL(name) | STT_FUNC | LOCAL | 로컬 함수 시작 |
SYM_FUNC_START_WEAK(name) | STT_FUNC | WEAK | 약한 심볼 함수 |
SYM_FUNC_END(name) | - | - | 함수 끝 (.size 자동 계산) |
SYM_CODE_START(name) | STT_NOTYPE | GLOBAL | 비표준 코드 (인터럽트 핸들러 등) |
SYM_CODE_END(name) | - | - | 비표준 코드 끝 |
SYM_DATA_START(name) | STT_OBJECT | GLOBAL | 데이터 블록 시작 |
SYM_DATA_END(name) | - | - | 데이터 블록 끝 |
SYM_DATA(name, data) | STT_OBJECT | GLOBAL | 단일 데이터 항목 |
SYM_INNER_LABEL(name, type) | 지정 | GLOBAL | 함수/코드 내부 레이블 |
/* SYM_FUNC_START가 생성하는 코드 (확장 결과) */
.global my_func
.type my_func, @function
.balign 16 /* 함수 시작 정렬 */
my_func:
.cfi_startproc
/* ... 함수 본문 ... */
ret
.cfi_endproc
.size my_func, .-my_func
/* SYM_CODE_START는 .cfi_startproc을 생략할 수 있음
* → 인터럽트 핸들러처럼 비표준 프레임을 가진 코드에 사용 */
아키텍처 조건부 코드
/* C 전처리기를 활용한 아키텍처 분기 (.S 파일) */
#ifdef CONFIG_X86_64
movq %rsp, %rdi
call x86_handler
#elif defined(CONFIG_ARM64)
mov x0, sp
bl arm64_handler
#elif defined(CONFIG_RISCV)
mv a0, sp
call riscv_handler
#endif
/* GAS 조건부 어셈블리 (.if 활용) */
.equ NR_CPUS, 256
.equ BITS_PER_LONG, 64
.if NR_CPUS > BITS_PER_LONG
/* 비트맵이 1 word보다 클 때 — 루프 탐색 */
xorl %ecx, %ecx
.Lscan:
bsfq (%rdi, %rcx, 8), %rax
jnz .Lfound
incq %rcx
cmpq $(NR_CPUS / BITS_PER_LONG), %rcx
jb .Lscan
.else
/* 비트맵이 1 word — 직접 bsf */
bsfq (%rdi), %rax
.endif
디버그 매크로 패턴
/* 디버그 빌드에서만 활성화되는 검증 매크로 */
.macro VERIFY_STACK_ALIGNMENT
#ifdef CONFIG_DEBUG_ENTRY
testl $0xF, %esp /* 16바이트 정렬 확인 */
jz .Laligned_\@
ud2 /* 정렬 위반 → 즉시 크래시 (디버그용) */
.Laligned_\@:
#endif
.endm
/* \@ 는 매크로 호출마다 고유 번호를 생성
* → 같은 매크로를 여러 번 호출해도 레이블 충돌 없음 */
.macro ANNOTATE_NOENDBR
.pushsection .discard.noendbr, ""
.quad . /* objtool이 이 위치를 ENDBR 면제로 인식 */
.popsection
.endm
GAS 모범 사례
커널 어셈블리 코드를 작성할 때 따라야 할 모범 사례를 정리합니다.
작성 규칙
| 규칙 | 좋은 예 | 나쁜 예 | 이유 |
|---|---|---|---|
| SYM_* 매크로 사용 | SYM_FUNC_START(foo) | ENTRY(foo) | 심볼 타입/크기 자동 설정 |
| .balign 사용 | .balign 16 | .align 4 | 아키텍처 독립적 해석 |
| RIP 상대 주소 | movq sym(%rip), %rax | movq sym, %rax | KASLR 호환, PIE 안전 |
| CFI 정보 일관성 | 모든 push/pop에 CFI 매칭 | CFI 누락 | 스택 트레이스 정확성 |
| .note.GNU-stack | 항상 포함 | 생략 | 스택 비실행 보장 |
| asm-offsets 사용 | TASK_THREAD_SP(%rdi) | 2776(%rdi) | 구조체 레이아웃 변경 대응 |
| 매크로 인자 이스케이프 | \reg | reg | 매크로 인자 참조 정확성 |
| 주석 스타일 | /* C 스타일 */ | # 해시 주석 | 아키텍처 이식성 |
성능 관련 팁
- 함수 정렬: 자주 호출되는 함수는
.balign 16또는.balign 64로 캐시라인(Cacheline) 경계에 정렬하세요. 커널의SYM_FUNC_START가 자동으로 정렬합니다. - 분기 예측: 오류 경로의 분기는 forward jump(앞으로 점프)로 작성하세요. x86 CPU는 forward jump를 "not taken"으로 예측합니다.
- PAUSE 명령어: 스핀 루프에서
pause명령어를 반드시 사용하세요. Hyper-Threading CPU에서 다른 스레드에 실행 자원을 양보합니다. - NOP 패딩: 핫패치(hotpatch) 사이트에는
.nops N지시자를 사용하여 최적 크기의 NOP을 자동 선택하게 하세요. - LOCK 프리픽스 최소화:
lock프리픽스는 메모리 버스를 잠그므로 성능 비용이 큽니다. 가능하면 per-CPU 변수나 lockless 알고리즘을 사용하세요.
관련 문서
공식 문서 및 참고 자료
- Using as — The GNU Assembler (sourceware.org) — GNU Assembler 공식 매뉴얼, 전체 지시자 및 문법 레퍼런스
- GAS Assembler Directives — Pseudo Ops (sourceware.org) —
.section,.global,.align등 모든 의사 명령어 목록 - x86 Machine-Dependent Features (sourceware.org) — x86/x86-64 전용 GAS 옵션, AT&T 문법 규칙, 접두사 처리
- AArch64 Machine-Dependent Features (sourceware.org) — ARM64 전용 GAS 지시자, 레지스터 표기, 재배치 수식자
- RISC-V Machine-Dependent Features (sourceware.org) — RISC-V 전용 GAS 옵션, ISA 확장 선택, 재배치 처리
- CFI Directives Reference (sourceware.org) —
.cfi_startproc,.cfi_def_cfa,.cfi_offset등 DWARF CFI 지시자 전체 레퍼런스 - GAS Macro Facility (sourceware.org) —
.macro/.endm매크로 정의, 인자, 재귀 사용법 - GAS Sections and Relocation (sourceware.org) — 섹션 지시자, 플래그, 링커 연동 방식
- GNU Binutils Documentation (sourceware.org) — as, ld, objdump, readelf, nm 등 Binutils 전체 문서 모음
- GNU Linker (ld) Manual (sourceware.org) — 링커 스크립트 문법, 섹션 배치, MEMORY/SECTIONS 명령, 심볼 해석
- Linker Scripts — LD (sourceware.org) — 링커 스크립트 상세 문법: SECTIONS, MEMORY, PHDRS, 출력 섹션 제어
- ELF Specification v1.2 (linuxfoundation.org) — Executable and Linkable Format 공식 명세, 섹션 헤더·세그먼트·재배치 형식
- x86-64 ELF ABI Supplement (linuxfoundation.org) — x86-64 재배치 유형, TLS 모델, PLT/GOT 구조
- DWARF v5 Specification (dwarfstd.org) — DWARF Debugging Information Format v5, CFI 프레임 정보 표준
- DWARF v4 Specification (dwarfstd.org) — DWARF v4 명세, 리눅스 커널이 주로 사용하는 DWARF 버전
- System V AMD64 ABI (psabi.org) — x86-64 호출 규약(Calling Convention), 레지스터 사용 규칙, 스택 프레임
- ARM Architecture ABI (github.com/ARM-software) — AArch64 프로시저 콜 표준(AAPCS64), ELF 재배치, DWARF 매핑
- RISC-V ELF psABI (github.com/riscv-non-isa) — RISC-V ELF 재배치 유형, 호출 규약, TLS 모델
- Linux Kernel Programming Language (kernel.org) — 커널 C/어셈블리 코딩 표준, GNU 확장 사용 지침
- GCC Extended Asm (gcc.gnu.org) — GCC 인라인 어셈블리 문법, 제약 조건(Constraints), 클로버(Clobber) 목록
- NASM Manual (nasm.us) — NASM/Intel 문법 레퍼런스, GAS AT&T 문법과의 비교 참고
- Intel 64 and IA-32 SDM (intel.com) — x86/x86-64 명령어 셋 전체 레퍼런스, 인코딩 규칙
- ARM Architecture Reference Manual (developer.arm.com) — ARMv8/ARMv9 A-profile 명령어 셋 아키텍처 레퍼런스
관련 페이지
- ELF & GNU Binutils — ar, nm, objcopy, objdump, readelf, strip 등 Binutils 도구 전체
- 어셈블리 종합 — GCC 인라인 어셈블리, AT&T/Intel 문법 비교, x86_64/ARM64 ABI
- GCC 완전 가이드 — GCC 컴파일 파이프라인, 최적화, 인라인 어셈블리
- ELF 파일 형식 — ELF 구조, 섹션 헤더, 심볼 테이블, 재배치 엔트리
- GDB 완전 가이드 — DWARF 활용, 소스 레벨 디버깅, CFI 정보 확인
- 메모리 배리어(Memory Barrier) — x86 TSO, ARM64 약한 메모리 모델, 배리어 명령어
관련 문서
- C 언어 완전 가이드 & 커널 C 관용어 — GNU C 확장과 커널 관용어를 중심으로 container_of·READ_ONCE·barr
- 커널 필수 함수·매크로·심볼 레퍼런스 — 커널 개발에서 반복적으로 쓰는 함수·매크로·심볼을 컨텍스트별로 무제한 확장 정리한 실전 레
- CPUID 명령어 (CPUID) — x86 CPUID: Leaf 구조, 피처 비트 해석, Intel/AMD 차이, boo