1. Exception
entry.S는 head.S와 같은 경로에 존재하며 arch/arm64/kernel/entry.S이다.
entry.S 에서는 Exception 처리에 관한 부분을 정의한다. arm에는 User, Knerel 등 여러 모드가 존재하고 각각의 모드는 Level로 나누어 ELn으로 나타낸다. 이러한 EL간의 이동은 자유롭지 않으며 특정 이벤트를 발생시켜 넘어갈 수 있다. 이러한 부분에 대한 정의를 진행하는 곳이 entry.S이다.
커널에 진입하기 위해서는 Exception, system call등을 이용하며 EL0 -> EL1 등으로 EL을 이동할 수 있다. 참고로 EL2, EL3도 마찬가지이며 EL1, EL2, EL3는 공통포맷을가지며 유저모드인 EL0만 다른 포맷을 유지한다.
정리하면 특정 EL에서 각각의 Level로 이동하기 위해서는 Exception, system call등의 이벤트가 발생해야 한다. 그리고 이러한 것들을 진행하면 각각의 레벨에 따른 Exception Table의 주소로 이동하게 된다. 주소로 이동하면 핸들러가 구현되어있는 주소를 가리키고 있으며 해당 핸들러 주소로 이동하여 동작을 수행한다.
EL에 따른 base address가 존재하며 이러한 각 레벨에 따른 base address를 저장하고 있는 레지스터가 VBAR_ELn이다. 예를들어 head.S에서 정의한 VBAR_EL1 레지스터에는 EL1에 대한 Exception Table에 접근하기위한 EL1레벨의 base address가 저장되어있다.
1) Exception vector table
Address | Exception type | Description |
VBAR_ELn + 0x000 | Synchronous | Current EL with SP0 |
+ 0x080 | IRQ/vIRQ | |
+ 0x100 | FIQ/vFIQ | |
+ 0x180 | SError/vSError | |
+ 0x200 | Synchronous | Current EL with SPx |
+ 0x280 | IRQ/vIRQ | |
+ 0x300 | FIQ/vFIQ | |
+ 0x380 | SError/vSError | |
+ 0x400 | Synchronous | Lower EL using AArch64 |
+ 0x480 | IRQ/vIRQ | |
+ 0x500 | FIQ/vFIQ | |
+ 0x580 | SError/vSError | |
+ 0x600 | Synchronous | Lower EL using AArch32 |
+ 0x680 | IRQ/vIRQ | |
+ 0x700 | FIQ/vFIQ | |
+ 0x780 | SError/vSError |
그리고 위와 같이 Exception vector table의 주소가 각 base address기준으로 정의되어 있다.
위 정의 내용을보면 가장 위 4개는 현재 Exception Level에서 Exception이 발생했고 이 때 SP_EL0를 사용하는 경우 IRQ, FIQ, SError에 따른 분기할 주소를 나타낸다.
그 다음 4개는 현재 Exception Level에서 발생했고 SP_El1을 사용하는 경우
다음 4개는 지금 Level보다 낮은 Exception Level에서 발생했고 64bit인 경우
마지막 4개는 지금 Level 보다 낮은 Exception Level에서 발생했고 32bit인 경우이다.
여기서 SP_ELn이란 Stack pointer 이며 Arm8v에서는 EL0~EL3까지 Exceptio Level에서 각각의 Stack pointer가 따로존재하고 각각의 SP값을 SP_EL0~SP_EL3에 저장한다. 마찬가지로 위에서 언급한바와 같이 base Level도 VBAR_ELn으로 Level에 따라 따로 관리하며 마지막으로 Exception처리 후 복귀할 주소를 저장하는 ELR Register도 Exception Level별로 각각 두고 있다. (Arm7v에서는 X30하나로 ARMv8의 ELR을 대체한다)
/* * Exception vectors. */ .pushsection".entry.text","ax" .align11 SYM_CODE_START(vectors) kernel_ventry1,t,64,sync// Synchronous EL1t kernel_ventry1,t,64,irq// IRQ EL1t kernel_ventry1,t,64,fiq// FIQ EL1t kernel_ventry1,t,64,error// Error EL1t kernel_ventry1,h,64,sync// Synchronous EL1h kernel_ventry1,h,64,irq// IRQ EL1h kernel_ventry1,h,64,fiq// FIQ EL1h kernel_ventry1,h,64,error// Error EL1h kernel_ventry0,t,64,sync// Synchronous 64-bit EL0 kernel_ventry0,t,64,irq// IRQ 64-bit EL0 kernel_ventry0,t,64,fiq// FIQ 64-bit EL0 kernel_ventry0,t,64,error// Error 64-bit EL0 kernel_ventry0,t,32,sync// Synchronous 32-bit EL0 kernel_ventry0,t,32,irq// IRQ 32-bit EL0 kernel_ventry0,t,32,fiq// FIQ 32-bit EL0 kernel_ventry0,t,32,error// Error 32-bit EL0 SYM_CODE_END(vectors) |
그리고 위의 vector table의 format을 직접적으로 메모리에 코딩한 부분이 entry.S의 위 코드로 보면 된다. 위 포맷의 EL1은 EL2, EL3도 같은 포맷으로 적용된다.
가장 먼저 kernel_ventry macro는 el, ht, regsize, label로 정의되어있다.
예를들어 kernel_ventry 1,h,64,sync는 EL1, h, 64 size, sync label로 정의하는것과 같은 의미를 지닌다. h는 아마 낮지않은 Exception Level에서 Exception이 발생하였음을 말하고 t는 현재보다 더 낮은 Exception Level에서 Exception이 발생하였음을 의미하는 것 같다.(?)
2) Exception 발생 시 이전 data 처리
Exception이 발생하게 되면 복귀 후 동작을 위해 공용으로 사용하는 레지스터의 값들을 미리 저장해둔다. 이 때 사용하는 매크로는 kernel_entry이다.
kernel_entry는 exception발생 시 가장 처음 무조건 들어가는 코드이며 이전에 사용하던 SP, PC, PSTATE값을 다음 레지스터에 저장한다.
/* * Registers that may be useful after this macro is invoked: * * x20 - ICC_PMR_EL1 * x21 - aborted SP * x22 - aborted PC * x23 - aborted PSTATE */ |
마찬가지로 Exceptio처리가 종료되면 x21, x22, x23에 저장된 값들을 다시 write해주어야 하는데 해당작업을 위한 매크로가 kernel_exit이다.
3) Exception handler
다시 위에서 정의한 Exception의 vector table을 보면 vector table에는 kernel_ventry 매크로가 정의되어있다 즉 낮은 Exception혹은 Current Exception에서 Exception이 발생하고 어떤 SP_EL 레지스터를 사용하는지 그리고 SError인지 IRQ인지 Sync인지 등에 따라서 구분되어 현재 상황에 맞는 kernel_ventry 가 실행된다. 이렇게 실행된 kernel_ventry 매크로 정의부를 살펴보면
위와 같이 jump하게 되고
.macro entry_handler el:req,ht:req,regsize:req,label:req SYM_CODE_START_LOCAL (el\el\ht\()_\regsize\()_\label) kernel_entry \el, \regsize mov x0, sp bl el \el\ht\()_\regsize\()_\label\()_handler .if \el==0 b ret_to_user .else b ret_to_kernel .endif SYM_CODE_END(el\el\ht\()_\regsize\()_\label) .endm |
jump된 주소(Exception handler매크로)는 위와 같은 명령을 실행한다. 가장 먼저 kernel_entry에 Exception level값과 regsize값을 전달해주어 SP, PSTATE 값들을 x21, x22, x23, x20 레지스터에 저장하는 작업을 진행하고 실제적으로 handler함수를 호출한다. 이 때 호출 문구를 kernel_ventry1,h,64,irq// IRQ EL1h
로 해석하면 bl el1h_64_irq_handler 명령어가 되고 이는 entry-common.c에 있는 아래 handler function을 호출한다.
asmlinkage void noinstr el1h_64_irq_handler(structpt_regs*regs) { el1_interrupt(regs,handle_arch_irq); } |
- el1h_64_sync_handler()
asmlinkage voidnoinstr el1h_64_sync_handler(structpt_regs *regs) { unsignedlongesr = read_sysreg(esr_el1); switch(ESR_ELx_EC(esr)) { caseESR_ELx_EC_DABT_CUR: caseESR_ELx_EC_IABT_CUR: el1_abort(regs, esr); break; /* * We don't handle ESR_ELx_EC_SP_ALIGN, since we will have hit a * recursive exception when trying to push the initial pt_regs. */ caseESR_ELx_EC_PC_ALIGN: el1_pc(regs, esr); break; caseESR_ELx_EC_SYS64: caseESR_ELx_EC_UNKNOWN: el1_undef(regs); break; caseESR_ELx_EC_BREAKPT_CUR: caseESR_ELx_EC_SOFTSTP_CUR: caseESR_ELx_EC_WATCHPT_CUR: caseESR_ELx_EC_BRK64: el1_dbg(regs, esr); break; caseESR_ELx_EC_FPAC: el1_fpac(regs, esr); break; default: __panic_unhandled(regs, "64-bit el1h sync", esr); } } |
가장먼저 ESR 레지스터 값을 가져온다 ESR 레지스터는 Exception 발생 이유를 나타내며 Sync Exception일 때만 정보가 제공된다.
때문에 Sync에서는 ESR 레지스터의 값을 얻어온 후 Switch문을 통해서 Exception 원인에 따른 처리과정을 보여준다.
el1_abort, el1_pc, el1_undef, el_dbg, el1_fpac은 각각의 원인에따른 signal sending or Error 로그와함께 system die등의 결과를 나타낸다.
위 코드들에 대한 정의는
arch/arm64/kernel/traps.c
arch/arm64/kernel/fault.c
에서 확인이 가능하다.
- el1h_64_irq_handler()
asmlinkage voidnoinstr el1h_64_irq_handler(structpt_regs *regs) { el1_interrupt(regs, handle_arch_irq); } |
위와 같이 handler 함수를 실행하는데 handler함수의 주소값은 미리 handle_arch_irq에 저장되어있고 이 값을 불러와서 호출한다.
해당 값은 device driver 초기화를 진행할 때 set_handle_irq()를 호출해서 주소 값을 handle_arch_irq에 저장한다. 보통의 경우 인터럽트 핸들러는 generic_handle_irq()를 호출하여 등록된 ISR을 수행한다. 해당 등록과정은 device tree와 연관이 되어있을 것 같다(?)
참고로 FIQ도 역시 IRQ와 동일하게 동작한다. 다만 핸들러가 handle_arch_fiq로 차이가 있다.
- el1h_64_error_handler()
asmlinkage voidnoinstr el1h_64_error_handler(structpt_regs *regs) { unsignedlongesr = read_sysreg(esr_el1); local_daif_restore(DAIF_ERRCTX); arm64_enter_nmi(regs); do_serror(regs, esr); arm64_exit_nmi(regs); } |
error의 경우 do_serror function내부로 들어가면 arch/arm64/kernel/traps.c내부에 존재하는 function을 통해 panic이 발생한다.
- el0t_64_sync_handler()
해당 핸들러는 EL0에서 실행되었으며 SVC모드를 통한 커널 모드로 진입하는 system call 과정을 나타낸다. 위에서 언급한 내용과 마찬가지로 EL0에서 Exception이 발생하여 위 function이 호출되면 ESR 레지스터를 읽고 ESR_Elx_EC_SVC64 값을 가지고 있는지 체크 한다. 해당 값이 가지고 있으면 User mode에서 Exception Level 상승을위한 동작이라고 생각하면 된다.
asmlinkage voidnoinstr el0t_64_sync_handler(structpt_regs *regs) { unsignedlongesr = read_sysreg(esr_el1); switch(ESR_ELx_EC(esr)) { caseESR_ELx_EC_SVC64: el0_svc(regs); break; ... } |
위와 같이 switch문을통해서 el0_svc()를 호출하게되고
staticvoidnoinstr el0_svc(structpt_regs *regs) { enter_from_user_mode(regs); cortex_a76_erratum_1463225_svc_handler(); do_el0_svc(regs); exit_to_user_mode(regs); } |
do_el0_svc() function을 통해 arch/arm64/kernel/syscall.c에 존재하는 el0_svc_common()을 호출 결과적으로 invoke_syscall() function을 통해서 kernel모드로 진입을 시도한다.
- ref
https://elixir.bootlin.com/linux/v5.14.16/source/arch/arm64/kernel/entry.S
https://elixir.bootlin.com/linux/v5.14.16/source/
https://developer.arm.com/documentation/100933/0100/AArch64-exception-vector-table
http://egloos.zum.com/rousalome/v/10033656
https://github.com/torvalds/linux/blob/master/arch/arm64/kernel/entry-common.c
'OS > Linux' 카테고리의 다른 글
[Linux Kernel] objdump, arm-eabi-addr2line (0) | 2021.12.04 |
---|---|
[Linux Kernel] Kernel 분석(v5.14.16) - Device tree (0) | 2021.11.25 |
[Linux Kernel] Kernel 분석(v5.14.16) - head.S (0) | 2021.11.07 |
[Linux Kernel] DT (디바이스 트리) (0) | 2021.01.09 |
[Linux] 메모리 영역 (2) - Heap (0) | 2020.12.26 |