GNU Assembler (as) 완전 가이드

GAS 문법을 단순 명령어 목록이 아닌 커널 코드 작성 관점으로 설명합니다. `.section`·`.type`·`.size`·재배치(Relocation) 지시자 의미, 매크로(Macro)/조건 조립으로 반복 코드 관리, CFI 기반 언와인드 정보 생성, x86·AArch64·RISC-V별 calling convention 차이, C 코드 inline asm과의 제약 조건 연결, 부트 코드·트랩 핸들러(Handler)·저수준 컨텍스트 전환 루틴 작성 시 흔한 오류와 점검법까지 상세히 정리합니다.

기준 버전: GNU Binutils 2.46 (2025년). 이 페이지(Page)는 Using as 공식 매뉴얼(470페이지)의 핵심 내용을 커널 개발 관점으로 정리합니다. 커맨드라인 도구 전반은 GNU Binutils를, GCC 파이프라인(Pipeline)과 인라인 어셈블리(Assembly)는 어셈블리 종합을 참고하세요.
전제 조건: GNU Binutils어셈블리 종합 문서를 먼저 읽으세요. ELF 파일 형식(섹션, 심볼 테이블(Symbol Table), 재배치)의 기본 개념을 알고 있으면 이 문서를 훨씬 빠르게 이해할 수 있습니다.
일상 비유: GNU Assembler는 번역가와 같습니다. 사람이 읽을 수 있는 어셈블리 텍스트(영어 원문)를 CPU가 직접 이해하는 기계어(Machine Code) 바이트(외국어 번역본)로 변환합니다. 번역 규칙(문법)을 알아야 번역가(어셈블러)와 올바르게 소통할 수 있습니다.

핵심 요약

  • 역할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 각각 고유 지시자와 확장 문법이 있습니다.

단계별 이해

  1. 기본 변환 이해
    as -o foo.o foo.s로 어셈블리 파일을 오브젝트로 변환하고 objdump -d foo.o로 결과를 확인합니다.
  2. 문법 규칙 익히기
    AT&T 문법의 레지스터(Register) 접두사(%), 상수 접두사($), 명령어 접미사(b/w/l/q)를 익힙니다.
  3. 지시자 활용
    .section으로 섹션을 지정하고, .global/.type/.size로 심볼 속성을 설정합니다.
  4. CFI 추가
    함수에 .cfi_startproc/.cfi_endproc을 추가하여 DWARF 언와인드 정보를 생성합니다.
  5. 아키텍처 확장
    필요한 아키텍처(-march=)와 확장(+sve 등)을 지정하여 최신 명령어를 활용합니다.

GNU Assembler 개요

GNU Assembler(as)는 GNU Binutils 패키지의 핵심 구성 요소로, 어셈블리 언어 소스 파일(.s, .S)을 ELF(Executable and Linkable Format) 오브젝트 파일(.o)로 변환합니다. GCC 컴파일 파이프라인에서 cc1(C 컴파일러 프론트엔드)이 생성한 임시 .s 파일을 as가 어셈블합니다.

소스 파일 foo.c / foo.s 전처리/컴파일 cpp / cc1 GNU as 어셈블러 오브젝트 foo.o (ELF) .s .s GNU Assembler 처리 흐름 gcc -S → .s 파일 → as → .o (ELF) → ld → 실행 파일

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화이트스페이스/주석 전처리 생략. 입력이 이미 처리된 경우에 사용
-gDWARF 디버그 정보 생성 (파일명, 라인 번호)
-I path.include 지시자의 검색 경로 추가
-K차이 테이블(difference table)이 넘치면 경고 출력
-L로컬 심볼(이름이 L로 시작)을 심볼 테이블에 포함
--listing-lhs-width=n리스팅 왼쪽 폭(바이트 수) 설정
--listing-rhs-width=n리스팅 오른쪽 폭(문자 수) 설정
-M / --mriMRI 호환 어셈블러 모드
--MD fileMakefile 의존성 파일 생성 (gcc -MD와 유사)
-o file출력 오브젝트 파일명 지정 (기본: a.out)
-Rdata 섹션을 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, %rbxrax, rbx
상수(즉시값)$42, $0x1f42, 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

심볼 명명 규칙

섹션과 재배치

어셈블러는 소스를 여러 섹션으로 나눠 처리합니다. 각 섹션은 ELF 파일의 섹션 헤더로 기록되며, 링커(Linker)가 최종 실행 파일을 생성할 때 배치를 결정합니다.

표준 섹션

섹션플래그설명
.textax (실행·할당)실행 코드 섹션
.dataaw (쓰기·할당)초기화된 읽기/쓰기 데이터
.rodataa (할당)읽기 전용 데이터 (상수)
.bssaw (쓰기·할당)미초기화 데이터 (파일에 공간 없음)
.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 symSTB_GLOBAL전역 심볼 — 다른 오브젝트 파일에서 참조 가능
.globl symSTB_GLOBAL.global의 동의어
.local symSTB_LOCAL로컬 심볼 — 현재 오브젝트에서만 유효
.weak symSTB_WEAK약한 심볼 — 강한 정의가 있으면 덮어써짐
.weakref alias, targetSTB_WEAK약한 참조 별칭 정의

심볼 가시성(Visibility) 지시자

지시자ELF 가시성설명
.protected symSTV_PROTECTED외부에서 보이지만 오버라이드 불가
.hidden symSTV_HIDDEN공유 라이브러리(Shared Library) 외부에서 보이지 않음
.internal symSTV_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으로 전환
.bssBSS 섹션으로 전환
.pushsection name [, "flags"]현재 섹션을 스택에 저장하고 섹션 전환
.popsection스택에서 이전 섹션 복원
.previous최근 이전 섹션으로 복귀
.subsection n현재 섹션의 하위 섹션 n으로 전환

데이터 지시자

지시자크기설명
.byte expr [, ...]1바이트1바이트 정수 상수 삽입
.word / .short expr2바이트2바이트 정수 상수
.long / .int expr4바이트4바이트 정수 상수
.quad expr8바이트8바이트 정수 상수
.octa expr16바이트16바이트 정수 상수
.float / .single expr4바이트IEEE 754 단정밀도 부동소수
.double expr8바이트IEEE 754 배정밀도 부동소수
.ascii "str"가변NULL 없는 문자열
.asciz "str" / .string "str"가변+1NULL 종료 문자열
.fill count [, size [, val]]가변count×size 바이트를 val로 채움. 기본: size=1, val=0
.space n [, val] / .skip n [, val]n바이트n바이트 공간 확보, val로 채움 (기본 0)
.zero nn바이트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 지시자 .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register rbp subq $32, %rsp ... movq %rbp, %rsp popq %rbp .cfi_def_cfa rsp, 8 ret .cfi_endproc 스택 프레임 구조 반환 주소 (caller가 push) 이전 %rbp (pushq %rbp) %rbp → 로컬 변수 영역 (subq $32, %rsp) %rsp → CFA (Canonical Frame Address) = 프레임 시작 전 %rsp 값 = %rbp + 16 (반환주소+saved rbp 크기)

주요 CFI 지시자

지시자설명
.cfi_startproc [simple]CFI 정보 블록 시작. simple: 기본 초기화 생략
.cfi_endprocCFI 정보 블록 종료
.cfi_def_cfa reg, offsetCFA = reg + offset으로 정의
.cfi_def_cfa_register regCFA 계산에 사용할 레지스터만 변경 (오프셋 유지)
.cfi_def_cfa_offset offsetCFA 오프셋만 변경 (레지스터 유지)
.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, reg2reg1의 이전 값이 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 sizeGNU 확장: 피우시(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=cpuCPU 타입 지정 (generic32, generic64, x86-64, znver4, sapphirerapids 등)
-mtune=cpu성능 최적화 타겟 CPU
-msse2avxSSE 명령어 인코딩을 VEX(AVX) 형식으로 출력
-mavxscalar=128AVX 스칼라 연산 크기 지정
-mno-shared공유 라이브러리 생성 안 함 (PLT/GOT 불필요)
-mamd64 / -mintel64AMD64 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 / addr1632/64비트 모드에서 16비트 피연산자/주소 강제
rex64REX.W 프리픽스 명시적 지정
notrackCET 간접 분기 추적 비활성화

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=lp64ILP32 vs LP64 ABI 선택
-EB / -EL빅 엔디안(Endianness) / 리틀 엔디안

아키텍처 확장 (+extension)

확장설명
+sveScalable Vector Extension (가변 길이 벡터)
+sve2SVE2 (확장 벡터 명령어)
+lseLarge System Extension (원자 연산 강화)
+mteMemory Tagging Extension
+btcBranch Target Identification
+rcpcRelease-Consistent Processor Consistent 로드
+crypto암호화(Encryption) 확장 (AES, SHA 등)
+dotprod점적(dot product) 명령어
+sm4SM4 암호화 명령어

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=rv64gcISA 문자열. rv32/rv64 + 확장 (g=IMAFD, c=압축)
-mabi=lp64dABI: ilp32, lp64, lp64d(하드 FP) 등
-mno-relax링커 릴랙세이션(instruction relaxation) 비활성화
-mcsr-checkCSR 읽기/쓰기 접근 권한 체크

RISC-V 머신 지시자

지시자설명
.option rvc압축(C) 명령어 허용
.option norvc압축 명령어 금지 (정렬 보장)
.option relax릴랙세이션 활성화 (기본)
.option norelax릴랙세이션 비활성화
.option picPIC 코드 생성 모드
.option nopic비-PIC 모드
.option push현재 옵션 상태 저장
.option pop저장된 옵션 상태 복원
.attribute tag, valueABI/아키텍처 속성 설정 (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 */
팁: GAS 표현식은 어셈블 시간에 평가되므로 런타임 비용이 없습니다. 매직 넘버 대신 .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 섹션 → 링커 스크립트 → vmlinux 배치 GAS 출력 섹션 (.o) .text (코드) .init.text (초기화 코드) .rodata (읽기 전용) .data (데이터) .bss (미초기화) .altinstructions __ex_table vmlinux.lds.S SECTIONS { .text : { *(.text) } .init : { *(.init.*) } .rodata : { ... } .data : { ... } .bss : { ... } } vmlinux (최종 ELF) _stext ... _etext 실행 코드 영역 __init_begin ... __init_end 부팅 후 해제 __start_rodata ... __end_rodata 읽기 전용 _sdata ... _edata 읽기/쓰기 데이터 __bss_start ... __bss_stop 0 초기화 영역

커널 특수 섹션과 GAS

커널은 표준 ELF 섹션 외에 다수의 특수 섹션을 정의합니다. 어셈블리에서 이 섹션들에 데이터를 삽입하려면 .pushsection/.popsection 패턴을 사용합니다.

섹션용도GAS에서 사용 예
.init.text부팅 시에만 실행, 이후 메모리 해제.section ".init.text", "ax"
.init.data초기화 데이터, 부팅 후 해제.section ".init.data", "aw"
.altinstructionsCPU 기능별 대체 명령어 테이블.pushsection .altinstructions, "a"
__ex_table예외 처리 테이블 (유저 메모리 접근).pushsection __ex_table, "a"
.fixup예외 발생 시 복구 코드.section .fixup, "ax"
__bug_tableBUG()/BUG_ON() 위치 기록.pushsection __bug_table, "aw"
.note.GNU-stack실행 가능 스택 비활성화 표시.section .note.GNU-stack, "", @progbits
__jump_tablestatic 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, nmGNU 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'
팁: CFI 정보가 올바른지 확인하는 가장 실용적인 방법은 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이 활성화된 커널에서 절대 주소는 반드시 실패합니다 */
KASLR 주의: Linux 커널은 기본적으로 KASLR(Kernel Address Space Layout Randomization)이 활성화됩니다. 모든 전역 심볼 접근은 RIP 상대 주소 지정(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)
왜 필요한가: C 구조체의 필드 오프셋은 컴파일러 버전, 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)가 분리되어 다양한 아키텍처를 지원합니다.

GNU Assembler 내부 파이프라인 프론트엔드 (아키텍처 공통) 렉서 / 토큰화 줄 분석, 심볼 추출 파서 / 지시자 해석 표현식 평가, 섹션 관리 심볼 테이블 구축 바인딩, 가시성, 크기 매크로 확장 .macro, .rept, .irp 전방 참조 해결 (2-패스) 패스 1: 크기 추정 → 패스 2: 확정 백엔드 (아키텍처 종속) 명령어 인코딩 opcode → 기계어 바이트 릴랙세이션 분기 크기 최적화 재배치 엔트리 생성 R_X86_64_PC32 등 DWARF / CFI 생성 .eh_frame, .debug_* ELF 오브젝트 출력 섹션 + 심볼 + 재배치 + DWARF 백엔드 모듈: tc-i386.c (x86) | tc-aarch64.c (ARM64) | tc-riscv.c (RISC-V) 각 모듈이 명령어 인코딩, 재배치 타입, 릴랙세이션 규칙을 정의

파이프라인 단계 상세

단계입력출력핵심 동작
전처리.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%ch4번째 인자
%rdx%edx%dx%dl%dh3번째 인자
%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~x764비트인자 / 반환값 (x0~x1 반환)
x864비트간접 반환 레지스터 (구조체 반환 주소)
x9~x1564비트임시 (caller-saved)
x16 (IP0)64비트인트라프로시저 콜 스크래치
x17 (IP1)64비트인트라프로시저 콜 스크래치
x1864비트플랫폼 레지스터 (커널: shadow call stack)
x19~x2864비트범용 (callee-saved)O
x29 (FP)64비트프레임 포인터O
x30 (LR)64비트링크 레지스터 (반환 주소)O
SP64비트스택 포인터 (16바이트 정렬 필수)O
XZR/WZR64/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 저장
x0zero하드와이어드 0-
x1ra반환 주소
x2sp스택 포인터O
x3gp글로벌 포인터-
x4tp스레드 포인터-
x5~x7t0~t2임시
x8s0/fp프레임 포인터 / callee-savedO
x9s1callee-savedO
x10~x11a0~a1인자 / 반환값
x12~x17a2~a7인자
x18~x27s2~s11callee-savedO
x28~x31t3~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 의사명령어

의사명령어실제 변환설명
nopaddi x0, x0, 0아무 동작 안 함
li rd, immlui + addi (조합)즉시값 로드 (임의 크기)
la rd, symbolauipc + addi주소 로드 (PC-relative)
mv rd, rsaddi rd, rs, 0레지스터 이동
not rd, rsxori rd, rs, -1비트 NOT
neg rd, rssub rd, x0, rs부정
j offsetjal x0, offset무조건 점프
call symbolauipc ra, ...; jalr ra, ...함수 호출 (원거리)
retjalr x0, 0(ra)함수 반환
beqz rs, offbeq rs, x0, off0이면 분기
bnez rs, offbne rs, x0, off0이 아니면 분기
seqz rd, rssltiu rd, rs, 10이면 1 설정
fencefence iorw, iorw전체 메모리 펜스

인라인 어셈블리 상세

GCC 확장 asm의 제약 조건(Constraint), 클로버(Clobber) 리스트, 명명 피연산자(Named Operand) 등 고급 기법을 다룹니다.

제약 조건 참조 (x86-64)

제약의미매핑 대상
r범용 레지스터RAX~R15 중 아무거나
aRAX/EAX/AX/AL%rax 계열
bRBX/EBX/BX/BL%rbx 계열
cRCX/ECX/CX/CL%rcx 계열
dRDX/EDX/DX/DL%rdx 계열
SRSI/ESI%rsi 계열
DRDI/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"해당 레지스터가 파괴됨명시적으로 사용하는 레지스터가 출력에 없을 때
volatile 필수 여부: 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 검증 활성화
커널 스택 언와인드: DWARF vs ORC vs Frame Pointer DWARF .eh_frame CIE + FDE 바이트코드 완전한 레지스터 복원 규칙 표준 형식 (GDB/perf 호환) 장점: 정확, 표준 단점: 파싱 복잡, 크기 큼 단점: 커널 패닉 시 불안정 유저스페이스 기본 방식 섹션: .eh_frame, .debug_frame ORC (커널 전용) 고정 크기 엔트리 (6바이트) SP + BP 복원만 기록 objtool이 자동 생성 장점: 빠름, 간결, 안정 장점: 패닉 시에도 동작 단점: x86-64 전용 x86-64 커널 기본 방식 섹션: .orc_unwind, .orc_lookup Frame Pointer 체인 RBP → 이전 RBP → ... 체인 각 프레임에서 RBP+8 = 반환 주소 추가 메타데이터 불필요 장점: 구현 단순, 이식성 장점: 모든 아키텍처 지원 단점: 성능 오버헤드 (~2%) 단점: RBP 범용 레지스터 손실 ARM64/RISC-V 커널 기본

커널 어셈블리 실전 패턴

커널 소스에서 반복적으로 나타나는 어셈블리 패턴을 아키텍처별로 정리합니다. 이 패턴들을 이해하면 커널 코드를 읽고 수정하는 데 큰 도움이 됩니다.

컨텍스트 전환 패턴

/* 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-64ARM64RISC-V
원자적 증가lock incl (%rdi)ldxr w0,[x1]; add w0,w0,#1; stxr w2,w0,[x1]amoadd.w zero,a0,(a1)
메모리 배리어mfencedmb ishfence rw,rw
시스템 콜syscallsvc #0ecall
인터럽트 비활성climsr DAIFSet, #2csrc sstatus, 2
NOPnop (0x90)nop (d503201f)nop (addi x0,x0,0)
함수 호출call funcbl funcjal ra, func
함수 반환retret (br x30)ret (jalr x0,0(ra))
스택 pushpushq %raxstp x0,x1,[sp,#-16]!addi sp,sp,-8; sd a0,0(sp)
TLB 무효화invlpg (%rdi)tlbi vale1is, x0sfence.vma x0, x0
캐시 플러시clflush (%rdi)dc civac, x0cbo.flush (a0)

매크로와 조건부 어셈블리

커널에서 사용하는 고급 매크로 패턴과 조건부 어셈블리 기법을 추가로 다룹니다.

SYM_* 매크로 체계

Linux 5.5부터 도입된 SYM_* 매크로는 어셈블리 심볼의 타입, 가시성, CFI 정보를 일관되게 관리합니다. 이전의 ENTRY()/END() 매크로를 대체합니다.

매크로심볼 타입가시성용도
SYM_FUNC_START(name)STT_FUNCGLOBAL일반 함수 시작 (CFI 자동 포함)
SYM_FUNC_START_LOCAL(name)STT_FUNCLOCAL로컬 함수 시작
SYM_FUNC_START_WEAK(name)STT_FUNCWEAK약한 심볼 함수
SYM_FUNC_END(name)--함수 끝 (.size 자동 계산)
SYM_CODE_START(name)STT_NOTYPEGLOBAL비표준 코드 (인터럽트 핸들러 등)
SYM_CODE_END(name)--비표준 코드 끝
SYM_DATA_START(name)STT_OBJECTGLOBAL데이터 블록 시작
SYM_DATA_END(name)--데이터 블록 끝
SYM_DATA(name, data)STT_OBJECTGLOBAL단일 데이터 항목
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), %raxmovq sym, %raxKASLR 호환, PIE 안전
CFI 정보 일관성모든 push/pop에 CFI 매칭CFI 누락스택 트레이스 정확성
.note.GNU-stack항상 포함생략스택 비실행 보장
asm-offsets 사용TASK_THREAD_SP(%rdi)2776(%rdi)구조체 레이아웃 변경 대응
매크로 인자 이스케이프\regreg매크로 인자 참조 정확성
주석 스타일/* 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 알고리즘을 사용하세요.

관련 문서

공식 문서 및 참고 자료

관련 페이지