요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
=============
Kernel Stacks
=============
Kernel stacks on x86-64 bit
===========================
Most of the text from Keith Owens, hacked by AK
x86_64 page size (PAGE_SIZE) is 4K.
Like all other architectures, x86_64 has a kernel stack for every
active thread. These thread stacks are THREAD_SIZE (4*PAGE_SIZE) big.
These stacks contain useful data as long as a thread is alive or a
zombie. While the thread is in user space the kernel stack is empty
except for the thread_info structure at the bottom.
In addition to the per thread stacks, there are specialized stacks
associated with each CPU. These stacks are only used while the kernel
is in control on that CPU; when a CPU returns to user space the
specialized stacks contain no useful data. The main CPU stacks are:
* Interrupt stack. IRQ_STACK_SIZE
Used for external hardware interrupts. If this is the first external
hardware interrupt (i.e. not a nested hardware interrupt) then the
kernel switches from the current task to the interrupt stack. Like
the split thread and interrupt stacks on i386, this gives more room
for kernel interrupt processing without having to increase the size
of every per thread stack.
The interrupt stack is also used when processing a softirq.
Switching to the kernel interrupt stack is done by software based on a
per CPU interrupt nest counter. This is needed because x86-64 "IST"
hardware stacks cannot nest without races.
x86_64 also has a feature which is not available on i386, the ability
to automatically switch to a new stack for designated events such as
double fault or NMI, which makes it easier to handle these unusual
events on x86_64. This feature is called the Interrupt Stack Table
(IST). There can be up to 7 IST entries per CPU. The IST code is an
index into the Task State Segment (TSS). The IST entries in the TSS
point to dedicated stacks; each stack can be a different size.
An IST is selected by a non-zero value in the IST field of an
interrupt-gate descriptor. When an interrupt occurs and the hardware
loads such a descriptor, the hardware automatically sets the new stack
pointer based on the IST value, then invokes the interrupt handler. If
the interrupt came from user mode, then the interrupt handler prologue
will switch back to the per-thread stack. If software wants to allow
nested IST interrupts then the handler must adjust the IST values on
entry to and exit from the interrupt handler. (This is occasionally
done, e.g. for debug exceptions.)
Events with different IST codes (i.e. with different stacks) can be
nested. For example, a debug interrupt can safely be interrupted by an
NMI. arch/x86_64/kernel/entry.S::paranoidentry adjusts the stack
pointers on entry to and exit from all IST events, in theory allowing
IST events with the same code to be nested. However in most cases, the
stack size allocated to an IST assumes no nesting for the same code.
If that assumption is ever broken then the stacks will become corrupt.
The currently assigned IST stacks are:
* ESTACK_DF. EXCEPTION_STKSZ (PAGE_SIZE).
Used for interrupt 8 - Double Fault Exception (#DF).
Invoked when handling one exception causes another exception. Happens
when the kernel is very confused (e.g. kernel stack pointer corrupt).
Using a separate stack allows the kernel to recover from it well enough
in many cases to still output an oops.
* ESTACK_NMI. EXCEPTION_STKSZ (PAGE_SIZE).
Used for non-maskable interrupts (NMI).
NMI can be delivered at any time, including when the kernel is in the
middle of switching stacks. Using IST for NMI events avoids making
assumptions about the previous state of the kernel stack.
* ESTACK_DB. EXCEPTION_STKSZ (PAGE_SIZE).
Used for hardware debug interrupts (interrupt 1) and for software
debug interrupts (INT3).
When debugging a kernel, debug interrupts (both hardware and
software) can occur at any time. Using IST for these interrupts
avoids making assumptions about the previous state of the kernel
stack.
To handle nested #DB correctly there exist two instances of DB stacks. On
#DB entry the IST stackpointer for #DB is switched to the second instance
so a nested #DB starts from a clean stack. The nested #DB switches
the IST stackpointer to a guard hole to catch triple nesting.
* ESTACK_MCE. EXCEPTION_STKSZ (PAGE_SIZE).
Used for interrupt 18 - Machine Check Exception (#MC).
MCE can be delivered at any time, including when the kernel is in the
middle of switching stacks. Using IST for MCE events avoids making
assumptions about the previous state of the kernel stack.
For more details see the Intel IA32 or AMD AMD64 architecture manuals.
Printing backtraces on x86
==========================
The question about the '?' preceding function names in an x86 stacktrace
keeps popping up, here's an indepth explanation. It helps if the reader
stares at print_context_stack() and the whole machinery in and around
arch/x86/kernel/dumpstack.c.
Adapted from Ingo's mail, Message-ID: <20150521101614.GA10889@gmail.com>:
We always scan the full kernel stack for return addresses stored on
the kernel stack(s) [1]_, from stack top to stack bottom, and print out
anything that 'looks like' a kernel text address.
If it fits into the frame pointer chain, we print it without a question
mark, knowing that it's part of the real backtrace.
If the address does not fit into our expected frame pointer chain we
still print it, but we print a '?'. It can mean two things:
- either the address is not part of the call chain: it's just stale
values on the kernel stack, from earlier function calls. This is
the common case.
- or it is part of the call chain, but the frame pointer was not set
up properly within the function, so we don't recognize it.
This way we will always print out the real call chain (plus a few more
entries), regardless of whether the frame pointer was set up correctly
or not - but in most cases we'll get the call chain right as well. The
entries printed are strictly in stack order, so you can deduce more
information from that as well.
The most important property of this method is that we _never_ lose
information: we always strive to print _all_ addresses on the stack(s)
that look like kernel text addresses, so if debug information is wrong,
we still print out the real call chain as well - just with more question
marks than ideal.
.. [1] For things like IRQ and IST stacks, we also scan those stacks, in
the right order, and try to cross from one stack into another
reconstructing the call chain. This works most of the time.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
x86-64 thread stack과 CPU별 전용 stack
1-23이 문서는 `SPDX-License-Identifier: GPL-2.0`으로 배포됩니다. 본문의 대부분은 Keith Owens의 글을 바탕으로 하며 AK가 수정했습니다.
x86_64의 page size(`PAGE_SIZE`)는 4K입니다. 다른 architecture와 마찬가지로 x86_64도 active thread마다 kernel stack을 가지며, thread stack 크기인 `THREAD_SIZE`는 `4*PAGE_SIZE`입니다. 이 stack은 thread가 살아 있거나 zombie인 동안 유용한 data를 보관합니다. thread가 user space에 있을 때 kernel stack은 맨 아래의 `thread_info` structure를 제외하면 비어 있습니다.
thread별 stack 외에 각 CPU와 연결된 specialized stack도 있습니다. 이 stack은 해당 CPU에서 kernel이 control을 갖는 동안에만 사용되며, CPU가 user space로 돌아가면 유용한 data를 담고 있지 않습니다.
interrupt stack
24-39주요 CPU stack 가운데 하나는 `IRQ_STACK_SIZE` 크기의 interrupt stack입니다.
외부 hardware interrupt를 처리할 때 사용합니다. 첫 번째 외부 hardware interrupt, 즉 nested hardware interrupt가 아닌 경우 kernel은 current task stack에서 interrupt stack으로 전환합니다. i386에서 thread stack과 interrupt stack을 나누는 것처럼, 모든 thread별 stack을 키우지 않고도 kernel interrupt 처리에 더 많은 공간을 제공합니다.
interrupt stack은 softirq를 처리할 때도 사용합니다.
kernel interrupt stack으로의 전환은 CPU별 interrupt nesting counter를 기준으로 software가 수행합니다. x86-64의 `IST` hardware stack은 race 없이 중첩될 수 없기 때문에 이 방식이 필요합니다.
Interrupt Stack Table 동작과 중첩
40-65x86_64에는 i386에 없는 기능이 있습니다. double fault나 NMI처럼 지정된 event에서 새 stack으로 자동 전환할 수 있어 비정상적인 event 처리가 쉬워집니다. 이 기능을 Interrupt Stack Table(IST)이라고 합니다.
CPU마다 최대 7개의 IST entry가 존재할 수 있습니다. IST code는 Task State Segment(TSS)의 index이며, TSS의 IST entry는 전용 stack을 가리킵니다. 각 stack의 크기는 서로 다를 수 있습니다.
interrupt-gate descriptor의 IST field에 0이 아닌 값을 지정하면 해당 IST가 선택됩니다. interrupt가 발생해 hardware가 descriptor를 load하면, hardware는 IST 값에 따라 새 stack pointer를 자동 설정한 뒤 interrupt handler를 호출합니다. user mode에서 온 interrupt라면 interrupt handler prologue가 다시 thread별 stack으로 전환합니다.
software가 nested IST interrupt를 허용하려면 handler 진입과 종료 시 IST 값을 조정해야 합니다. debug exception 같은 경우에 이 작업을 수행합니다.
서로 다른 IST code, 즉 서로 다른 stack을 사용하는 event는 중첩될 수 있습니다. 예를 들어 debug interrupt는 NMI에 의해 안전하게 interrupt될 수 있습니다. `arch/x86_64/kernel/entry.S::paranoidentry`는 모든 IST event의 진입과 종료 때 stack pointer를 조정해 이론상 같은 code의 IST event도 중첩할 수 있게 합니다.
대부분의 IST stack 크기는 같은 code의 event가 중첩되지 않는다고 가정합니다. 이 가정이 깨지면 stack이 손상됩니다.
현재 할당된 IST stack
66-108현재 할당된 IST stack은 다음과 같습니다.
- `ESTACK_DF`: `EXCEPTION_STKSZ`(`PAGE_SIZE`), interrupt 8 Double Fault Exception(`#DF`)
- `ESTACK_NMI`: `EXCEPTION_STKSZ`(`PAGE_SIZE`), non-maskable interrupt(NMI)
- `ESTACK_DB`: `EXCEPTION_STKSZ`(`PAGE_SIZE`), hardware debug interrupt 1과 software debug interrupt(`INT3`)
- `ESTACK_MCE`: `EXCEPTION_STKSZ`(`PAGE_SIZE`), interrupt 18 Machine Check Exception(`#MC`)
`ESTACK_DF`는 한 exception을 처리하다가 다른 exception이 발생할 때 호출됩니다. kernel stack pointer가 손상되는 등 kernel 상태가 심각하게 꼬였을 때 발생합니다. 별도 stack을 사용하면 많은 경우 kernel이 oops를 출력할 정도까지는 복구할 수 있습니다.
`ESTACK_NMI`는 kernel이 stack을 전환하는 도중을 포함해 언제든 전달될 수 있는 NMI를 처리합니다. NMI event에 IST를 사용하면 이전 kernel stack 상태를 가정하지 않아도 됩니다.
`ESTACK_DB`는 언제든 발생할 수 있는 hardware 및 software debug interrupt를 처리합니다. IST를 사용하면 이전 kernel stack 상태를 가정하지 않아도 됩니다.
nested `#DB`를 올바르게 처리하기 위해 DB stack은 두 instance로 존재합니다. `#DB` 진입 시 `#DB`용 IST stack pointer를 두 번째 instance로 전환하므로 nested `#DB`가 깨끗한 stack에서 시작합니다. 다시 중첩된 `#DB`는 IST stack pointer를 guard hole로 옮겨 세 번째 중첩을 탐지합니다.
`ESTACK_MCE`는 kernel이 stack을 전환하는 도중에도 전달될 수 있는 Machine Check Exception을 처리합니다. MCE event에 IST를 사용하면 이전 kernel stack 상태를 가정하지 않아도 됩니다. 자세한 내용은 Intel IA32 또는 AMD AMD64 architecture manual을 참고하십시오.
x86 backtrace의 물음표
109-129x86 stacktrace에서 function name 앞에 붙는 `?`의 의미는 반복해서 제기되는 질문입니다. 자세히 이해하려면 `print_context_stack()`과 `arch/x86/kernel/dumpstack.c` 안팎의 전체 처리 구조를 살펴보는 것이 도움이 됩니다.
이 설명은 Ingo의 mail `Message-ID: <20150521101614.GA10889@gmail.com>`을 바탕으로 정리했습니다.
kernel은 stack의 맨 위에서 맨 아래까지 kernel stack에 저장된 return address를 항상 전부 scan하고, kernel text address처럼 보이는 모든 값을 출력합니다.
주소가 frame pointer chain에 들어맞으면 실제 backtrace의 일부임을 알 수 있으므로 물음표 없이 출력합니다. 예상한 frame pointer chain에 맞지 않더라도 주소는 출력하되 `?`를 붙입니다.
물음표 항목의 의미와 정보 보존
130-152`?`가 붙은 주소는 두 가지를 의미할 수 있습니다.
- 주소가 call chain의 일부가 아니며 이전 function call이 kernel stack에 남긴 오래된 값일 수 있습니다. 이것이 일반적인 경우입니다.
- 주소가 call chain의 일부이지만 function 안에서 frame pointer가 올바르게 설정되지 않아 kernel이 이를 인식하지 못했을 수 있습니다.
이 방식은 frame pointer가 올바르게 설정되었는지와 관계없이 실제 call chain을 언제나 출력하며, 몇 개의 추가 entry가 함께 나올 수 있습니다. 대부분의 경우 call chain도 정확히 식별합니다. entry는 stack 순서 그대로 출력되므로 그 순서에서도 추가 정보를 추론할 수 있습니다.
이 방법의 가장 중요한 성질은 정보를 절대 잃지 않는다는 점입니다. kernel text address처럼 보이는 stack의 모든 주소를 출력하려고 하므로 debug information이 잘못되어도 실제 call chain을 함께 보여 줍니다. 다만 이상적인 경우보다 물음표가 더 많이 붙을 수 있습니다.
IRQ stack이나 IST stack도 올바른 순서로 scan하며, 한 stack에서 다른 stack으로 이동해 call chain을 재구성하려고 합니다. 이 방식은 대부분의 경우 동작합니다.
요약과 해설
kernel-stacks.rst:1-152x86-64는 thread별 kernel stack 외에 CPU별 interrupt stack과 최대 7개의 IST entry를 사용합니다. IST는 double fault, NMI, debug, machine check처럼 현재 stack을 신뢰하기 어려운 event에서 hardware가 전용 stack으로 전환하도록 합니다.
backtrace는 정보 손실을 피하기 위해 kernel text address처럼 보이는 값을 모두 출력합니다. frame pointer chain과 일치하지 않는 항목에는 `?`가 붙으며, 이는 stale value일 수도 있고 frame pointer가 불완전한 실제 call chain 항목일 수도 있습니다.