Part 1
요약·해설
새 ISA·ABI port에 필요한 boot, interrupt, scheduling, TLS, driver, power, MMU, stack, userspace, debugging 구현 항목을 한 문서에서 연결합니다.
boot · IRQ · context switch
MMU/MPU · stack · userspace
fault policy · GDB stub
Part 2
접을 수 있는 영어 원문 전체
영어 원문 전체 펼치기
C73F34D9CA168232AA28FE49FC49F8E859AF8E54D16DBF99590E685340884332
.. _architecture_porting_guide:
Architecture Porting Guide
##########################
An architecture port is needed to enable Zephyr to run on an :abbr:`ISA
(instruction set architecture)` or an :abbr:`ABI (Application Binary
Interface)` that is not currently supported.
The following are examples of ISAs and ABIs that Zephyr supports:
* x86_32 ISA with System V ABI
* ARMv7-M ISA with Thumb2 instruction set and ARM Embedded ABI (aeabi)
* ARCv2 ISA
For information on Kconfig configuration, see
:ref:`setting_configuration_values`. Architectures use a Kconfig configuration
scheme similar to boards.
An architecture port can be divided in several parts; most are required and
some are optional:
* **The early boot sequence**: each architecture has different steps it must
take when the CPU comes out of reset (required).
* **Interrupt and exception handling**: each architecture handles asynchronous
and unrequested events in a specific manner (required).
* **Thread context switching**: the Zephyr context switch is dependent on the
ABI and each ISA has a different set of registers to save (required).
* **Thread creation and termination**: A thread's initial stack frame is ABI
and architecture-dependent, and thread abortion possibly as well (required).
* **Device drivers**: most often, the system clock timer and the interrupt
controller are tied to the architecture (some required, some optional).
* **Utility libraries**: some common kernel APIs rely on a
architecture-specific implementation for performance reasons (required).
* **CPU idling/power management**: most architectures implement instructions
for putting the CPU to sleep (partly optional, most likely very desired).
* **Fault management**: for implementing architecture-specific debug help and
handling of fatal error in threads (partly optional).
* **Linker scripts and toolchains**: architecture-specific details will most
likely be needed in the build system and when linking the image (required).
* **Memory Management and Memory Mapping**: for architecture-specific details
on supporting memory management and memory mapping.
* **Stack Objects**: for architecture-specific details on memory protection
hardware regarding stack objects.
* **User Mode Threads**: for supporting threads in user mode.
* **GDB Stub**: for supporting GDB stub to enable remote debugging.
Early Boot Sequence
*******************
The goal of the early boot sequence is to take the system from the state it is
after reset to a state where is can run C code and thus the common kernel
initialization sequence. Most of the time, very few steps are needed, while
some architectures require a bit more work to be performed.
Common steps for all architectures:
* Setup an initial stack.
* If running an :abbr:`XIP (eXecute-In-Place)` kernel, copy initialized data
from ROM to RAM.
* If not using an ELF loader, zero the BSS section.
* Jump to :code:`z_cstart()`, the early kernel initialization
* :code:`z_cstart()` is responsible for context switching out of the fake
context running at startup into the main thread.
Some examples of architecture-specific steps that have to be taken:
* If given control in real mode on x86_32, switch to 32-bit protected mode.
* Setup the segment registers on x86_32 to handle boot loaders that leave them
in an unknown or broken state.
* Initialize a board-specific watchdog on Cortex-M3/4.
* Switch stacks from MSP to PSP on Cortex-M.
* Use a different approach than calling into _Swap() on Cortex-M to prevent
race conditions.
* Setup FIRQ and regular IRQ handling on ARCv2.
Interrupt and Exception Handling
********************************
Each architecture defines interrupt and exception handling differently.
When a device wants to signal the processor that there is some work to be done
on its behalf, it raises an interrupt. When a thread does an operation that is
not handled by the serial flow of the software itself, it raises an exception.
Both, interrupts and exceptions, pass control to a handler. The handler is
known as an :abbr:`ISR (Interrupt Service Routine)` in the case of
interrupts. The handler performs the work required by the exception or the
interrupt. For interrupts, that work is device-specific. For exceptions, it
depends on the exception, but most often the core kernel itself is responsible
for providing the handler.
The kernel has to perform some work in addition to the work the handler itself
performs. For example:
* Prior to handing control to the handler:
* Save the currently executing context.
* Possibly getting out of power saving mode, which includes waking up
devices.
* Updating the kernel uptime if getting out of tickless idle mode.
* After getting control back from the handler:
* Decide whether to perform a context switch.
* When performing a context switch, restore the context being context
switched in.
This work is conceptually the same across architectures, but the details are
completely different:
* The registers to save and restore.
* The processor instructions to perform the work.
* The numbering of the exceptions.
* etc.
It thus needs an architecture-specific implementation, called the
interrupt/exception stub.
Another issue is that the kernel defines the signature of ISRs as:
.. code-block:: C
void (*isr)(void *parameter)
Architectures do not have a consistent or native way of handling parameters to
an ISR. As such there are two commonly used methods for handling the
parameter.
* Using some architecture defined mechanism, the parameter value is forced in
the stub. This is commonly found in X86-based architectures.
* The parameters to the ISR are inserted and tracked via a separate table
requiring the architecture to discover at runtime which interrupt is
executing. A common interrupt handler demuxer is installed for all entries of
the real interrupt vector table, which then fetches the device's ISR and
parameter from the separate table. This approach is commonly used in the ARC
and ARM architectures via the :kconfig:option:`CONFIG_GEN_ISR_TABLES` implementation.
You can find examples of the stubs by looking at :code:`_interrupt_enter()` in
x86, :code:`_IntExit()` in ARM, :code:`_isr_wrapper()` in ARM, or the full
implementation description for ARC in :zephyr_file:`arch/arc/core/isr_wrapper.S`.
Each architecture also has to implement primitives for interrupt control:
* locking interrupts: :c:macro:`irq_lock()`, :c:macro:`irq_unlock()`.
* registering interrupts: :c:macro:`IRQ_CONNECT()`.
* programming the priority if possible :c:func:`irq_priority_set`.
* enabling/disabling interrupts: :c:macro:`irq_enable()`, :c:macro:`irq_disable()`.
.. note::
:c:macro:`IRQ_CONNECT` is a macro that uses assembler and/or linker script
tricks to connect interrupts at build time, saving boot time and text size.
The vector table should contain a handler for each interrupt and exception that
can possibly occur. The handler can be as simple as a spinning loop. However,
we strongly suggest that handlers at least print some debug information. The
information helps figuring out what went wrong when hitting an exception that
is a fault, like divide-by-zero or invalid memory access, or an interrupt that
is not expected (:dfn:`spurious interrupt`). See the ARM implementation in
:zephyr_file:`arch/arm/core/cortex_m/fault.c` for an example.
Thread Context Switching
************************
Multi-threading is the basic purpose to have a kernel at all. Zephyr supports
two types of threads: preemptible and cooperative.
Two crucial concepts when writing an architecture port are the following:
* Cooperative threads run at a higher priority than preemptible ones, and
always preempt them.
* After handling an interrupt, if a cooperative thread was interrupted, the
kernel always goes back to running that thread, since it is not preemptible.
A context switch can happen in several circumstances:
* When a thread executes a blocking operation, such as taking a semaphore that
is currently unavailable.
* When a preemptible thread unblocks a thread of higher priority by releasing
the object on which it was blocked.
* When an interrupt unblocks a thread of higher priority than the one currently
executing, if the currently executing thread is preemptible.
* When a thread runs to completion.
* When a thread causes a fatal exception and is removed from the running
threads. For example, referencing invalid memory,
Therefore, the context switching must thus be able to handle all these cases.
The kernel keeps the next thread to run in a "cache", and thus the context
switching code only has to fetch from that cache to select which thread to run.
There are two types of context switches: :dfn:`cooperative` and :dfn:`preemptive`.
* A *cooperative* context switch happens when a thread willfully gives the
control to another thread. There are two cases where this happens
* When a thread explicitly yields.
* When a thread tries to take an object that is currently unavailable and is
willing to wait until the object becomes available.
* A *preemptive* context switch happens either because an ISR or a
thread causes an operation that schedules a thread of higher priority than the
one currently running, if the currently running thread is preemptible.
An example of such an operation is releasing an object on which the thread
of higher priority was waiting.
.. note::
Control is never taken from cooperative thread when one of them is the
running thread.
A cooperative context switch is always done by having a thread call the
:code:`_Swap()` kernel internal symbol. When :code:`_Swap` is called, the
kernel logic knows that a context switch has to happen: :code:`_Swap` does not
check to see if a context switch must happen. Rather, :code:`_Swap` decides
what thread to context switch in. :code:`_Swap` is called by the kernel logic
when an object being operated on is unavailable, and some thread
yielding/sleeping primitives.
.. note::
On x86 and Nios2, :code:`_Swap` is generic enough and the architecture
flexible enough that :code:`_Swap` can be called when exiting an interrupt
to provoke the context switch. This should not be taken as a rule, since
neither the ARM Cortex-M or ARCv2 port do this.
Since :code:`_Swap` is cooperative, the caller-saved registers from the ABI are
already on the stack. There is no need to save them in the k_thread structure.
A context switch can also be performed preemptively. This happens upon exiting
an ISR, in the kernel interrupt exit stub:
* :code:`_interrupt_enter` on x86 after the handler is called.
* :code:`_IntExit` on ARM.
* :code:`_firq_exit` and :code:`_rirq_exit` on ARCv2.
In this case, the context switch must only be invoked when the interrupted
thread was preemptible, not when it was a cooperative one, and only when the
current interrupt is not nested.
The kernel also has the concept of "locking the scheduler". This is a concept
similar to locking the interrupts, but lighter-weight since interrupts can
still occur. If a thread has locked the scheduler, is it temporarily
non-preemptible.
So, the decision logic to invoke the context switch when exiting an interrupt
is simple:
* If the interrupted thread is not preemptible, do not invoke it.
* Else, fetch the cached thread from the ready queue, and:
* If the cached thread is not the current thread, invoke the context switch.
* Else, do not invoke it.
This is simple, but crucial: if this is not implemented correctly, the kernel
will not function as intended and will experience bizarre crashes, mostly due
to stack corruption.
.. note::
If running a coop-only system, i.e. if :kconfig:option:`CONFIG_NUM_PREEMPT_PRIORITIES`
is 0, no preemptive context switch ever happens. The interrupt code can be
optimized to not take any scheduling decision when this is the case.
Thread Creation and Termination
*******************************
To start a new thread, a stack frame must be constructed so that the context
switch can pop it the same way it would pop one from a thread that had been
context switched out. This is to be implemented in an architecture-specific
:code:`_new_thread` internal routine.
The thread entry point is also not to be called directly, i.e. it should not be
set as the :abbr:`PC (program counter)` for the new thread. Rather it must be
wrapped in :code:`_thread_entry`. This means that the PC in the stack
frame shall be set to :code:`_thread_entry`, and the thread entry point shall
be passed as the first parameter to :code:`_thread_entry`. The specifics of
this depend on the ABI.
The need for an architecture-specific thread termination implementation depends
on the architecture. There is a generic implementation, but it might not work
for a given architecture.
One reason that has been encountered for having an architecture-specific
implementation of thread termination is that aborting a thread might be
different if aborting because of a graceful exit or because of an exception.
This is the case for ARM Cortex-M, where the CPU has to be taken out of handler
mode if the thread triggered a fatal exception, but not if the thread
gracefully exits its entry point function.
This means implementing an architecture-specific version of
:c:func:`k_thread_abort`, and setting the Kconfig option
:kconfig:option:`CONFIG_ARCH_HAS_THREAD_ABORT` as needed for the architecture (e.g. see
:zephyr_file:`arch/arm/core/cortex_m/Kconfig`).
Thread Local Storage
********************
To enable thread local storage on a new architecture:
#. Implement :c:func:`arch_tls_stack_setup` to setup the TLS storage area in
stack. Refer to the toolchain documentation on how the storage area needs
to be structured. Some helper functions can be used:
* Function :c:func:`z_tls_data_size` returns the size
needed for thread local variables (excluding any extra data required by
toolchain and architecture).
* Function :c:func:`z_tls_copy` prepares the TLS storage area for
thread local variables. This only copies the variable themselves and
does not do architecture and/or toolchain specific data.
#. In the context switching, grab the ``tls`` field inside the new thread's
``struct k_thread`` and put it into an appropriate register (or some
other variable) for access to the TLS storage area. Refer to toolchain
and architecture documentation on which registers to use.
#. In kconfig, add ``select CONFIG_ARCH_HAS_THREAD_LOCAL_STORAGE`` to
kconfig related to the new architecture.
#. Run the ``tests/kernel/threads/tls`` to make sure the new code works.
Device Drivers
**************
The kernel requires very few hardware devices to function. In theory, the only
required device is the interrupt controller, since the kernel can run without a
system clock. In practice, to get access to most, if not all, of the sanity
check test suite, a system clock is needed as well. Since these two are usually
tied to the architecture, they are part of the architecture port.
Interrupt Controllers
=====================
There can be significant differences between the interrupt controllers and the
interrupt concepts across architectures.
For example, x86 has the concept of an :abbr:`IDT (Interrupt Descriptor Table)`
and different interrupt controllers. The position of an interrupt in the IDT
determines its priority.
On the other hand, the ARM Cortex-M has the :abbr:`NVIC (Nested Vectored
Interrupt Controller)` as part of the architecture definition. There is no need
for an IDT-like table that is separate from the NVIC vector table. The position
in the table has nothing to do with priority of an IRQ: priorities are
programmable per-entry.
The ARCv2 has its interrupt unit as part of the architecture definition, which
is somewhat similar to the NVIC. However, where ARC defines interrupts as
having a one-to-one mapping between exception and interrupt numbers (i.e.
exception 1 is IRQ1, and device IRQs start at 16), ARM has IRQ0 being
equivalent to exception 16 (and weirdly enough, exception 1 can be seen as
IRQ-15).
All these differences mean that very little, if anything, can be shared between
architectures with regards to interrupt controllers.
System Clock
============
x86 has APIC timers and the HPET as part of its architecture definition. ARM
Cortex-M has the SYSTICK exception. Finally, ARCv2 has the timer0/1 device.
Kernel timeouts are handled in the context of the system clock timer driver's
interrupt handler.
Console Over Serial Line
========================
There is one other device that is almost a requirement for an architecture
port, since it is so useful for debugging. It is a simple polling, output-only,
serial port driver on which to send the console (:code:`printk`,
:code:`printf`) output.
It is not required, and a RAM console (:kconfig:option:`CONFIG_RAM_CONSOLE`)
can be used to send all output to a circular buffer that can be read
by a debugger instead.
Utility Libraries
*****************
The kernel depends on a few functions that can be implemented with very few
instructions or in a lock-less manner in modern processors. Those are thus
expected to be implemented as part of an architecture port.
* Atomic operators.
* If instructions do exist for a given architecture, the implementation is
configured using the :kconfig:option:`CONFIG_ATOMIC_OPERATIONS_ARCH` Kconfig
option.
* If instructions do not exist for a given architecture,
a generic version that wraps :c:func:`irq_lock` or :c:func:`irq_unlock`
around non-atomic operations exists. It is configured using the
:kconfig:option:`CONFIG_ATOMIC_OPERATIONS_C` Kconfig option.
* Find-least-significant-bit-set and find-most-significant-bit-set.
* If instructions do not exist for a given architecture, it is always
possible to implement these functions as generic C functions.
It is possible to use compiler built-ins to implement these, but be careful
they use the required compiler barriers.
CPU Idling/Power Management
***************************
The kernel provides support for CPU power management with two functions:
:c:func:`arch_cpu_idle` and :c:func:`arch_cpu_atomic_idle`.
:c:func:`arch_cpu_idle` can be as simple as calling the power saving
instruction for the architecture with interrupts unlocked, for example
:code:`hlt` on x86, :code:`wfi` or :code:`wfe` on ARM, :code:`sleep` on ARC.
This function can be called in a loop within a context that does not care if it
get interrupted or not by an interrupt before going to sleep. There are
basically two scenarios when it is correct to use this function:
* In a single-threaded system, in the only thread when the thread is not used
for doing real work after initialization, i.e. it is sitting in a loop doing
nothing for the duration of the application.
* In the idle thread.
:c:func:`arch_cpu_atomic_idle`, on the other hand, must be able to atomically
re-enable interrupts and invoke the power saving instruction. It can thus be
used in real application code, again in single-threaded systems.
Normally, idling the CPU should be left to the idle thread, but in some very
special scenarios, these APIs can be used by applications.
Both functions must exist for a given architecture. However, the implementation
can be simply the following steps, if desired:
#. unlock interrupts
#. NOP
However, a real implementation is strongly recommended.
Fault Management
****************
In the event of an unhandled CPU exception, the architecture
code must call into :c:func:`z_fatal_error`. This function dumps
out architecture-agnostic information and makes a policy
decision on what to do next by invoking :c:func:`k_sys_fatal_error`.
This function can be overridden to implement application-specific
policies that could include locking interrupts and spinning forever
(the default implementation) or even powering off the
system (if supported).
Toolchain and Linking
*********************
Toolchain support has to be added to the build system.
Some architecture-specific definitions are needed in :zephyr_file:`include/zephyr/toolchain/gcc.h`.
See what exists in that file for currently supported architectures.
Each architecture also needs its own linker script, even if most sections can
be derived from the linker scripts of other architectures. Some sections might
be specific to the new architecture, for example the SCB section on ARM and the
IDT section on x86.
Memory Management and Memory Mapping
************************************
If the target platform enables paging and requires drivers to memory-map
their I/O regions, :kconfig:option:`CONFIG_MMU` needs to be enabled and the
following API implemented:
- :c:func:`arch_mem_map`
- :c:func:`arch_mem_unmap`
- :c:func:`arch_page_phys_get`
Stack Objects
*************
The presence of memory protection hardware affects how stack objects are
created. All architecture ports must specify the required alignment of the
stack pointer, which is some combination of CPU and ABI requirements. This
is defined in architecture headers with :c:macro:`ARCH_STACK_PTR_ALIGN` and
is typically something small like 4, 8, or 16 bytes.
Two types of thread stacks exist:
- "kernel" stacks defined with :c:macro:`K_KERNEL_STACK_DEFINE()` and related
APIs, which can host kernel threads running in supervisor mode or
used as the stack for interrupt/exception handling. These have significantly
relaxed alignment requirements and use less reserved data. No memory is
reserved for privilege elevation stacks.
- "thread" stacks which typically use more memory, but are capable of hosting
thread running in user mode, as well as any use-cases for kernel stacks.
If :kconfig:option:`CONFIG_USERSPACE` is not enabled, "thread" and "kernel" stacks are
equivalent.
Additional macros may be defined in the architecture layer to specify
the alignment of the base of stack objects, any reserved data inside the
stack object not used for the thread's stack buffer, and how to round up
stack sizes to support user mode threads. In the absence of definitions
some defaults are assumed:
- :c:macro:`ARCH_KERNEL_STACK_RESERVED`: default no reserved space
- :c:macro:`ARCH_THREAD_STACK_RESERVED`: default no reserved space
- :c:macro:`ARCH_KERNEL_STACK_OBJ_ALIGN`: default align to
:c:macro:`ARCH_STACK_PTR_ALIGN`
- :c:macro:`ARCH_THREAD_STACK_OBJ_ALIGN`: default align to
:c:macro:`ARCH_STACK_PTR_ALIGN`
- :c:macro:`ARCH_THREAD_STACK_SIZE_ALIGN`: default round up to
:c:macro:`ARCH_STACK_PTR_ALIGN`
All stack creation macros are defined in terms of these.
Stack objects all have the following layout, with some regions potentially
zero-sized depending on configuration. There are always two main parts:
reserved memory at the beginning, and then the stack buffer itself. The
bounds of some areas can only be determined at runtime in the context of
its associated thread object. Other areas are entirely computable at build
time.
Some architectures may need to carve-out reserved memory at runtime from the
stack buffer, instead of unconditionally reserving it at build time, or to
supplement an existing reserved area (as is the case with the ARM FPU).
Such carve-outs will always be tracked in ``thread.stack_info.start``.
The region specified by ``thread.stack_info.start`` and
``thread.stack_info.size`` is always fully accessible by a user mode thread.
``thread.stack_info.delta`` denotes an offset which can be used to compute
the initial stack pointer from the very end of the stack object, taking into
account storage for TLS and ASLR random offsets.
.. code-block:: none
+---------------------+ <- thread.stack_obj
| Reserved Memory | } K_(THREAD|KERNEL)_STACK_RESERVED
+---------------------+
| Carved-out memory |
|.....................| <- thread.stack_info.start
| Unused stack buffer |
| |
|.....................| <- thread's current stack pointer
| Used stack buffer |
| |
|.....................| <- Initial stack pointer. Computable
| ASLR Random offset | with thread.stack_info.delta
+---------------------| <- thread.userspace_local_data
| Thread-local data |
+---------------------+ <- thread.stack_info.start + thread.stack_info.size
At present, Zephyr does not support stacks that grow upward.
No Memory Protection
====================
If no memory protection is in use, then the defaults are sufficient.
HW-based stack overflow detection
=================================
This option uses hardware features to generate a fatal error if a thread
in supervisor mode overflows its stack. This is useful for debugging, although
for a couple reasons, you can't reliably make any assertions about the state
of the system after this happens:
* The kernel could have been inside a critical section when the overflow
occurs, leaving important global data structures in a corrupted state.
* For systems that implement stack protection using a guard memory region,
it's possible to overshoot the guard and corrupt adjacent data structures
before the hardware detects this situation.
To enable the :kconfig:option:`CONFIG_HW_STACK_PROTECTION` feature, the system must
provide some kind of hardware-based stack overflow protection, and enable the
:kconfig:option:`CONFIG_ARCH_HAS_STACK_PROTECTION` option.
Two forms of HW-based stack overflow detection are supported: dedicated
CPU features for this purpose, or special read-only guard regions immediately
preceding stack buffers.
:kconfig:option:`CONFIG_HW_STACK_PROTECTION` only catches stack overflows for
supervisor threads. This is not required to catch stack overflow from user
threads; :kconfig:option:`CONFIG_USERSPACE` is orthogonal.
This feature only detects supervisor mode stack overflows, including stack
overflows when handling system calls. It doesn't guarantee that the kernel has
not been corrupted. Any stack overflow in supervisor mode should be treated as
a fatal error, with no assertions about the integrity of the overall system
possible.
Stack overflows in user mode are recoverable (from the kernel's perspective)
and require no special configuration; :kconfig:option:`CONFIG_HW_STACK_PROTECTION`
only applies to catching overflows when the CPU is in supervisor mode.
CPU-based stack overflow detection
----------------------------------
If we are detecting stack overflows in supervisor mode via special CPU
registers (like ARM's SPLIM), then the defaults are sufficient.
Guard-based stack overflow detection
------------------------------------
We are detecting supervisor mode stack overflows via special memory protection
region located immediately before the stack buffer that generates an exception
on write. Reserved memory will be used for the guard region.
:c:macro:`ARCH_KERNEL_STACK_RESERVED` should be defined to the minimum size
of a memory protection region. On most ARM CPUs this is 32 bytes.
:c:macro:`ARCH_KERNEL_STACK_OBJ_ALIGN` should also be set to the required
alignment for this region.
MMU-based systems should not reserve RAM for the guard region and instead
simply leave an non-present virtual page below every stack when it is mapped
into the address space. The stack object will still need to be properly aligned
and sized to page granularity.
.. code-block:: none
+-----------------------------+ <- thread.stack_obj
| Guard reserved memory | } K_KERNEL_STACK_RESERVED
+-----------------------------+
| Guard carve-out |
|.............................| <- thread.stack_info.start
| Stack buffer |
. .
Guard carve-outs for kernel stacks are uncommon and should be avoided if
possible. They tend to be needed for two situations:
* The same stack may be re-purposed to host a user thread, in which case
the guard is unnecessary and shouldn't be unconditionally reserved.
This is the case when privilege elevation stacks are not inside the stack
object.
* The required guard size is variable and depends on context. For example, some
ARM CPUs have lazy floating point stacking during exceptions and may
decrement the stack pointer by a large value without writing anything,
completely overshooting a minimally-sized guard and corrupting adjacent
memory. Rather than unconditionally reserving a larger guard, the extra
memory is carved out if the thread uses floating point.
User mode enabled
=================
Enabling user mode activates two new requirements:
* A separate fixed-sized privilege mode stack, specified by
:kconfig:option:`CONFIG_PRIVILEGED_STACK_SIZE`, must be allocated that the user
thread cannot access. It is used as the stack by the kernel when handling
system calls. If stack guards are implemented, a stack guard region must
be able to be placed before it, with support for carve-outs if necessary.
* The memory protection hardware must be able to program a region that exactly
covers the thread's stack buffer, tracked in ``thread.stack_info``. This
implies that :c:macro:`ARCH_THREAD_STACK_SIZE_ADJUST()` will need to round
up the requested stack size so that a region may cover it, and that
:c:macro:`ARCH_THREAD_STACK_OBJ_ALIGN()` is also specified per the
granularity of the memory protection hardware.
This becomes more complicated if the memory protection hardware requires that
all memory regions be sized to a power of two, and aligned to their own size.
This is common on older MPUs and is known with
:kconfig:option:`CONFIG_MPU_REQUIRES_POWER_OF_TWO_ALIGNMENT`.
``thread.stack_info`` always tracks the user-accessible part of the stack
object, it must always be correct to program a memory protection region with
user access using the range stored within.
Non power-of-two memory region requirements
-------------------------------------------
On systems without power-of-two region requirements, the reserved memory area
for threads stacks defined by :c:macro:`K_THREAD_STACK_RESERVED` may be used to
contain the privilege mode stack. The layout could be something like:
.. code-block:: none
+------------------------------+ <- thread.stack_obj
| Other platform data |
+------------------------------+
| Guard region (if enabled) |
+------------------------------+
| Guard carve-out (if needed) |
|..............................|
| Privilege elevation stack |
+------------------------------| <- thread.stack_obj +
| Stack buffer | K_THREAD_STACK_RESERVED =
. . thread.stack_info.start
The guard region, and any carve-out (if needed) would be configured as a
read-only region when the thread is created.
* If the thread is a supervisor thread, the privilege elevation region is just
extra stack memory. An overflow will eventually crash into the guard region.
* If the thread is running in user mode, a memory protection region will be
configured to allow user threads access to the stack buffer, but nothing
before or after it. An overflow in user mode will crash into the privilege
elevation stack, which the user thread has no access to. An overflow when
handling a system call will crash into the guard region.
On an MMU system there should be no physical guards; the privilege mode stack
will be mapped into kernel memory, and the stack buffer in the user part of
memory, each with non-present virtual guard pages below them to catch runtime
stack overflows.
Other platform data may be stored before the guard region, but this is highly
discouraged if such data could be stored in ``thread.arch`` somewhere.
:c:macro:`ARCH_THREAD_STACK_RESERVED` will need to be defined to capture
the size of the reserved region containing platform data, privilege elevation
stacks, and guards. It must be appropriately sized such that an MPU region
to grant user mode access to the stack buffer can be placed immediately
after it.
Power-of-two memory region requirements
---------------------------------------
Thread stack objects must be sized and aligned to the same power of two,
without any reserved memory to allow efficient packing in memory. Thus,
any guards in the thread stack must be completely carved out, and the
privilege elevation stack must be allocated elsewhere.
:c:macro:`ARCH_THREAD_STACK_SIZE_ADJUST()` and
:c:macro:`ARCH_THREAD_STACK_OBJ_ALIGN()` should both be defined to
:c:macro:`Z_POW2_CEIL()`. :c:macro:`K_THREAD_STACK_RESERVED` must be 0.
For the privilege stacks, the :kconfig:option:`CONFIG_GEN_PRIV_STACKS` must be,
enabled. For every thread stack found in the system, a corresponding fixed-
size kernel stack used for handling system calls is generated. The address
of the privilege stacks can be looked up quickly at runtime based on the
thread stack address using :c:func:`z_priv_stack_find()`. These stacks are
laid out the same way as other kernel-only stacks.
.. code-block:: none
+-----------------------------+ <- z_priv_stack_find(thread.stack_obj)
| Reserved memory | } K_KERNEL_STACK_RESERVED
+-----------------------------+
| Guard carve-out (if needed) |
|.............................|
| Privilege elevation stack |
| |
+-----------------------------+ <- z_priv_stack_find(thread.stack_obj) +
K_KERNEL_STACK_RESERVED +
CONFIG_PRIVILEGED_STACK_SIZE
+-----------------------------+ <- thread.stack_obj
| MPU guard carve-out |
| (supervisor mode only) |
|.............................| <- thread.stack_info.start
| Stack buffer |
. .
The guard carve-out in the thread stack object is only used if the thread is
running in supervisor mode. If the thread drops to user mode, there is no guard
and the entire object is used as the stack buffer, with full access to the
associated user mode thread and ``thread.stack_info`` updated appropriately.
User Mode Threads
*****************
To support user mode threads, several kernel-to-arch APIs need to be
implemented, and the system must enable the :kconfig:option:`CONFIG_ARCH_HAS_USERSPACE`
option. Please see the documentation for each of these functions for more
details:
* :c:func:`arch_buffer_validate` to test whether the current thread has
access permissions to a particular memory region
* :c:func:`arch_user_mode_enter` which will irreversibly drop a supervisor
thread to user mode privileges. The stack must be wiped.
* :c:func:`arch_syscall_oops` which generates a kernel oops when system
call parameters can't be validated, in such a way that the oops appears to be
generated from where the system call was invoked in the user thread
* :c:func:`arch_syscall_invoke0` through
:c:func:`arch_syscall_invoke6` invoke a system call with the
appropriate number of arguments which must all be passed in during the
privilege elevation via registers.
* :c:func:`arch_is_user_context` return nonzero if the CPU is currently
running in user mode
* :c:func:`arch_mem_domain_max_partitions_get` which indicates the max
number of regions for a memory domain. MMU systems have an unlimited amount,
MPU systems have constraints on this.
Some architectures may need to update software memory management structures
or modify hardware registers on another CPU when memory domain APIs are invoked.
If so, :kconfig:option:`CONFIG_ARCH_MEM_DOMAIN_SYNCHRONOUS_API` must be selected by the
architecture and some additional APIs must be implemented. This is common
on MMU systems and uncommon on MPU systems:
* :c:func:`arch_mem_domain_thread_add`
* :c:func:`arch_mem_domain_thread_remove`
* :c:func:`arch_mem_domain_partition_add`
* :c:func:`arch_mem_domain_partition_remove`
Please see the doxygen documentation of these APIs for details.
In addition to implementing these APIs, there are some other tasks as well:
* :c:func:`_new_thread` needs to spawn threads with :c:macro:`K_USER` in
user mode
* On context switch, the outgoing thread's stack memory should be marked
inaccessible to user mode by making the appropriate configuration changes in
the memory management hardware.. The incoming thread's stack memory should
likewise be marked as accessible. This ensures that threads can't mess with
other thread stacks.
* On context switch, the system needs to switch between memory domains for
the incoming and outgoing threads.
* Thread stack areas must include a kernel stack region. This should be
inaccessible to user threads at all times. This stack will be used when
system calls are made. This should be fixed size for all threads, and must
be large enough to handle any system call.
* A software interrupt or some kind of privilege elevation mechanism needs to
be established. This is closely tied to how the _arch_syscall_invoke macros
are implemented. On system call, the appropriate handler function needs to
be looked up in _k_syscall_table. Bad system call IDs should jump to the
:c:enum:`K_SYSCALL_BAD` handler. Upon completion of the system call, care
must be taken not to leak any register state back to user mode.
GDB Stub
********
To enable GDB stub for remote debugging on a new architecture:
#. Create a new ``gdbstub.h`` header file under appropriate architecture
include directory (``include/arch/<arch>/gdbstub.h``).
* Create a new struct ``struct gdb_ctx`` as the GDB context.
* Must define a member named ``exception`` of type ``unsigned int`` to
store the GDB exception reason. This value needs to be set before
entering :c:func:`z_gdb_main_loop`.
* Architecture can define as many members as needed for GDB stub to
function.
* Pointer to this struct needs to be passed to :c:func:`z_gdb_main_loop`,
where this pointer will be passed to other GDB stub functions.
#. Functions for entering and exiting GDB stub main loop.
* If the architecture relies on interrupts to service breakpoints,
interrupt service routines (ISR) need to be implemented, which
will serve as the entry point to GDB stub main loop.
* These functions need to save and restore context so code execution
can continue as if no breakpoints have been encountered.
* These functions need to call :c:func:`z_gdb_main_loop` after saving
execution context to go into the GDB stub main loop to receive commands
from GDB.
* Before calling :c:func:`z_gdb_main_loop`, :c:member:`gdb_ctx.exception`
must be set to specify the exception reason.
#. Implement necessary functions to support GDB stub functionality:
* :c:func:`arch_gdb_init`
* This needs to initialize necessary bits to support GDB stub functionality,
for example, setting up the GDB context and connecting debug interrupts.
* This must stop code execution via architecture specific method (e.g.
raising debug interrupts). This allows GDB to connect during boot.
* :c:func:`arch_gdb_continue`
* This function is called when GDB sends a ``c`` or ``continue`` command
to continue code execution.
* :c:func:`arch_gdb_step`
* This function is called when GDB sends a ``si`` or ``stepi`` command
to execute one machine instruction, before returning to GDB prompt.
* Hardware register read/write functions:
* Since the GDB stub is running on the target, manipulation of hardware
registers need to cached to avoid affecting the execution of GDB stub.
Think of it as context switching, where the execution context is
changed to the GDB stub. So that the register values of the running
thread before context switch need to be stored. Manipulation of
register values must only be done to this cached copy. The updated
values will then be written to hardware registers before switching
back to the previous running thread.
* :c:func:`arch_gdb_reg_readall`
* This collects all hardware register values that would appear in
a ``g``/``G`` packets which will be sent back to GDB. The format of
the G-packet is architecture specific. Consult GDB on what is
expected.
* Note that, for most architectures, a valid G-packet must be returned
and sent to GDB. If a packet without incorrect length is sent to
GDB, GDB will abort the debugging session.
* :c:func:`arch_gdb_reg_writeall`
* This takes a G-packet sent by GDB and populates the hardware
registers with values from the G-packet.
* :c:func:`arch_gdb_reg_readone`
* This reads the value of one hardware register and sends
the result to GDB.
* :c:func:`arch_gdb_reg_writeone`
* This writes the value of one hardware register received from GDB.
* Breakpoints:
* :c:func:`arch_gdb_add_breakpoint` and
:c:func:`arch_gdb_remove_breakpoint`
* GDB may decide to use software breakpoints which modifies
the memory at the breakpoint locations to replace the instruction
with software breakpoint or trap instructions. GDB will then
restore the memory content once execution reaches the breakpoints.
GDB supports this by default and there is usually no need to
handle software breakpoints in the architecture code (where
breakpoint type is ``0``).
* Hardware breakpoints (type ``1``) are required if the code is
in ROM or flash that cannot be modified at runtime. Consult
the architecture datasheet on how to enable hardware breakpoints.
* If hardware breakpoints are not supported by the architecture,
there is no need to implement these in architecture code.
GDB will then rely on software breakpoints.
#. For architecture where certain memory regions are not accessible,
an array named :c:var:`gdb_mem_region_array` of type
:c:struct:`gdb_mem_region` needs to be defined to specify regions
that are accessible. For each array item:
* :c:member:`gdb_mem_region.start` specifies the start of a memory
region.
* :c:member:`gdb_mem_region.end` specifies the end of a memory
region.
* :c:member:`gdb_mem_region.attributes` specifies the permission
of a memory region.
* :c:macro:`GDB_MEM_REGION_RO`: region is read-only.
* :c:macro:`GDB_MEM_REGION_RW`: region is read-write.
* :c:member:`gdb_mem_region.alignment` specifies read/write alignment
of a memory region. Use ``0`` if there is no alignment requirement
and read/write can be done byte-by-byte.
API Reference
*************
Timing
======
.. doxygengroup:: arch-timing
Threads
=======
.. doxygengroup:: arch-threads
.. doxygengroup:: arch-tls
Power Management
================
.. doxygengroup:: arch-pm
Symmetric Multi-Processing
==========================
.. doxygengroup:: arch-smp
Interrupts
==========
.. doxygengroup:: arch-irq
Userspace
=========
.. doxygengroup:: arch-userspace
Memory Management
=================
.. doxygengroup:: arch-mmu
Miscellaneous Architecture APIs
===============================
.. doxygengroup:: arch-misc
GDB Stub APIs
=============
.. doxygengroup:: arch-gdbstub
Part 3
한국어 전문 번역
현재 지원하지 않는 ISA(instruction set architecture) 또는 ABI(Application Binary Interface)에서 Zephyr를 실행하려면 architecture port가 필요합니다.
Zephyr가 지원하는 조합의 예는 다음과 같습니다.
- System V ABI를 사용하는 x86_32 ISA
- Thumb2 instruction set과 ARM Embedded ABI(aeabi)를 사용하는 ARMv7-M ISA
- ARCv2 ISA
Kconfig 구성 방법은 Setting configuration values 문서를 참고합니다. Architecture도 board와 비슷한 Kconfig 구성 방식을 사용합니다.
Architecture port는 다음 부분으로 나뉩니다. 대부분 필수이고 일부는 선택 사항입니다.
- Early boot sequence: CPU가 reset에서 빠져나온 뒤 architecture별로 필요한 단계를 수행합니다. 필수입니다.
- Interrupt and exception handling: 비동기·비요청 event를 architecture 방식으로 처리합니다. 필수입니다.
- Thread context switching: Zephyr context switch는 ABI에 의존하고 ISA마다 저장할 register가 다릅니다. 필수입니다.
- Thread creation and termination: 초기 stack frame과 경우에 따라 thread abort도 ABI·architecture에 의존합니다. 필수입니다.
- Device drivers: system clock timer와 interrupt controller는 대개 architecture에 묶입니다. 일부 필수, 일부 선택입니다.
- Utility libraries: 성능을 위해 일부 공통 kernel API에 architecture별 구현이 필요합니다. 필수입니다.
- CPU idling/power management: CPU sleep instruction을 연결합니다. 일부는 선택이지만 실제 port에는 매우 유용합니다.
- Fault management: architecture별 debug 정보와 thread fatal error 처리를 구현합니다. 일부 선택입니다.
- Linker scripts and toolchains: build system과 image link에 architecture별 정의가 필요합니다. 필수입니다.
- Memory management and mapping: architecture의 memory management와 mapping을 지원합니다.
- Stack objects: memory-protection hardware 조건에 맞게 stack object를 정의합니다.
- User mode threads: user mode thread 실행을 지원합니다.
- GDB Stub: remote debugging용 GDB stub를 지원합니다.
Early Boot Sequence
Early boot sequence의 목표는 reset 직후 상태를 C code와 공통 kernel initialization sequence를 실행할 수 있는 상태로 바꾸는 것입니다. 대부분의 architecture는 몇 단계만 필요하지만 더 많은 준비가 필요한 경우도 있습니다.
모든 architecture에 공통인 단계는 다음과 같습니다.
- 초기 stack을 설정합니다.
- XIP(eXecute-In-Place) kernel이면 initialized data를 ROM에서 RAM으로 복사합니다.
- ELF loader를 사용하지 않으면 BSS section을 0으로 초기화합니다.
- 초기 kernel initialization인
z_cstart()로 jump합니다.z_cstart()는 startup의 가짜 context에서 main thread로 context switch하는 일도 담당합니다.
Architecture별 추가 단계의 예는 다음과 같습니다.
- x86_32에서 real mode로 제어권을 받았다면 32-bit protected mode로 전환합니다.
- x86_32의 segment register를 설정해 bootloader가 남긴 알 수 없거나 깨진 상태를 정리합니다.
- Cortex-M3/4에서 board별 watchdog을 초기화합니다.
- Cortex-M에서 MSP stack에서 PSP stack으로 전환합니다.
- Cortex-M에서는 race condition을 피하려고
_Swap()을 직접 호출하는 방식과 다른 접근을 사용합니다. - ARCv2의 FIRQ와 일반 IRQ 처리를 설정합니다.
Interrupt and Exception Handling
Interrupt와 exception 처리 방식은 architecture마다 다릅니다. Device가 processor에게 대신 처리할 일이 있다고 알릴 때 interrupt를 일으킵니다. Thread가 software의 직렬 흐름 자체로 처리되지 않는 연산을 수행하면 exception이 발생합니다.
두 event 모두 handler로 제어를 넘깁니다. Interrupt의 handler는 ISR(Interrupt Service Routine)이라고 합니다. ISR의 작업은 device별로 다르고, exception handler의 작업은 exception 종류에 따라 다르지만 보통 core kernel이 handler를 제공합니다.
Kernel은 handler 자체의 작업 외에도 다음 처리를 해야 합니다.
- Handler 진입 전: 현재 실행 context를 저장하고, 필요하면 power-saving mode에서 빠져나오며 device를 깨웁니다. Tickless idle에서 복귀한다면 kernel uptime도 갱신합니다.
- Handler 반환 후: context switch 여부를 결정하고, switch한다면 들어올 context를 복원합니다.
개념은 architecture 전체에서 같지만 저장·복원할 register, 실행 instruction, exception 번호 등이 완전히 다릅니다. 따라서 interrupt/exception stub라는 architecture별 구현이 필요합니다.
Kernel이 정의하는 ISR signature는 다음과 같습니다.
void (*isr)(void *parameter)Architecture에는 ISR parameter를 다루는 일관된 native 방식이 없어서 보통 다음 두 방법 중 하나를 사용합니다.
- Architecture가 정의한 mechanism으로 parameter 값을 stub 안에 강제로 넣습니다. x86 계열에서 흔합니다.
- ISR과 parameter를 별도 table에 넣고 추적합니다. Runtime에 현재 interrupt를 알아낸 뒤 실제 vector table의 모든 entry에 설치한 공통 demultiplexer가 별도 table에서 device ISR과 parameter를 꺼냅니다. ARC와 ARM은
CONFIG_GEN_ISR_TABLES구현으로 이 방식을 사용합니다. 예는 x86의_interrupt_enter(), ARM의_IntExit()·_isr_wrapper(), ARC의arch/arc/core/isr_wrapper.S에 있습니다.
각 architecture는 interrupt control primitive도 구현해야 합니다.
- Interrupt lock:
irq_lock(),irq_unlock() - Interrupt 등록:
IRQ_CONNECT() - 가능한 경우 priority program:
irq_priority_set() - Interrupt enable/disable:
irq_enable(),irq_disable()
Vector table에는 발생 가능한 모든 interrupt와 exception용 handler가 있어야 합니다. 단순 무한 loop도 가능하지만 최소한 debug 정보를 출력하는 편을 강하게 권장합니다. Divide-by-zero나 invalid-memory-access fault, 예상하지 못한 spurious interrupt를 분석하는 데 필요합니다. ARM 예는 `arch/arm/core/cortex_m/fault.c`입니다.
Thread Context Switching
Multi-threading은 kernel의 기본 목적입니다. Zephyr thread는 preemptible과 cooperative 두 종류입니다.
Architecture port에서 중요한 규칙은 두 가지입니다. Cooperative thread는 preemptible thread보다 priority가 높아 항상 이를 선점합니다. 또 interrupt가 cooperative thread를 중단했다면, 이 thread는 선점할 수 없으므로 handler 뒤에 항상 같은 thread로 돌아갑니다.
Context switch는 다음 상황에서 일어날 수 있습니다.
- Thread가 현재 사용할 수 없는 semaphore 획득 같은 blocking operation을 수행할 때
- Preemptible thread가 기다리던 object를 release해 더 높은 priority thread의 block을 풀 때
- Interrupt가 현재 실행 thread보다 높은 priority thread의 block을 풀고, 현재 thread가 preemptible일 때
- Thread 실행이 완료됐을 때
- Invalid memory reference 같은 fatal exception으로 thread가 실행 대상에서 제거될 때
따라서 context-switch code는 이 모든 경우를 처리해야 합니다. Kernel은 다음에 실행할 thread를 cache에 보관하므로 switch code는 그 cache에서 thread를 가져오면 됩니다.
Context switch는 cooperative와 preemptive 두 종류입니다.
- Cooperative: thread가 명시적으로 yield하거나, 사용할 수 없는 object를 기다리기로 하면서 자발적으로 다른 thread에 제어를 넘깁니다.
- Preemptive: ISR 또는 thread의 operation이 현재보다 높은 priority thread를 schedule하고 현재 thread가 preemptible일 때 발생합니다. 높은 priority thread가 기다리던 object를 release하는 것이 한 예입니다.
Cooperative switch는 thread가 kernel 내부 symbol _Swap()을 호출해 수행합니다. _Swap()이 호출된 시점에는 switch가 필요하다는 사실을 이미 kernel logic이 알고 있으므로 필요 여부를 다시 검사하지 않고 어떤 thread를 들여올지만 결정합니다. 사용할 수 없는 object를 다루거나 yield·sleep하는 kernel path가 이 symbol을 호출합니다.
Cooperative _Swap 호출 시 ABI의 caller-saved register는 이미 stack에 있으므로 k_thread 구조에 따로 저장할 필요가 없습니다.
Preemptive context switch는 ISR을 빠져나오는 kernel interrupt-exit stub에서도 수행합니다. x86은 handler 호출 뒤 `_interrupt_enter`, ARM은 `_IntExit`, ARCv2는 `_firq_exit`와 `_rirq_exit`가 해당 지점입니다.
이때 중단된 thread가 preemptible이고 현재 interrupt가 nested가 아닐 때만 switch해야 합니다. Scheduler lock은 interrupt lock보다 가벼운 개념으로 interrupt는 계속 발생하지만 해당 thread를 일시적으로 non-preemptible로 만듭니다.
Interrupt exit의 결정 절차는 단순합니다.
- 중단된 thread가 preemptible이 아니면 switch하지 않습니다.
- 그렇다면 ready queue에서 cached thread를 가져옵니다.
- Cached thread가 current thread와 다르면 context switch하고, 같으면 그대로 돌아갑니다.
이 logic은 단순하지만 결정적입니다. 잘못 구현하면 kernel이 의도대로 동작하지 않고, 주로 stack corruption 때문에 이해하기 어려운 crash가 발생합니다.
Thread Creation and Termination
새 thread를 시작하려면, context switch가 중단된 thread의 frame을 pop하는 것과 똑같이 pop할 수 있는 stack frame을 만들어야 합니다. Architecture별 내부 routine _new_thread가 이를 구현합니다.
Thread entry point를 직접 호출하거나 새 thread의 PC(program counter)로 바로 설정하면 안 됩니다. PC는 _thread_entry로 설정하고 실제 entry point는 ABI에 맞춰 그 첫 번째 parameter로 전달해야 합니다.
Thread termination의 architecture별 구현 필요 여부는 architecture에 달려 있습니다. Generic 구현이 있지만 모든 architecture에서 동작한다고 보장할 수 없습니다.
Graceful exit와 exception 때문에 abort하는 절차가 다를 수 있기 때문입니다. ARM Cortex-M에서는 thread가 fatal exception을 일으켰다면 CPU를 handler mode에서 빼내야 하지만 entry-point function이 정상 종료됐다면 그 작업이 필요 없습니다.
필요하면 architecture별 k_thread_abort()를 구현하고 CONFIG_ARCH_HAS_THREAD_ABORT를 설정합니다. 예는 arch/arm/core/cortex_m/Kconfig에 있습니다.
Thread Local Storage
새 architecture에서 TLS(thread local storage)를 활성화하는 순서는 다음과 같습니다.
arch_tls_stack_setup()으로 stack 안의 TLS storage area를 설정합니다. Toolchain 문서에 정의된 구조를 따릅니다.z_tls_data_size()는 toolchain·architecture 부가 data를 제외한 thread-local variable 공간 크기를 반환하고,z_tls_copy()는 variable 자체를 TLS area에 복사합니다.- Context switch 때 새 thread의
struct k_thread안tlsfield를 읽어 TLS 접근에 사용하는 register 또는 다른 변수에 넣습니다. 사용할 register는 toolchain·architecture 문서를 따릅니다. - 새 architecture Kconfig가
CONFIG_ARCH_HAS_THREAD_LOCAL_STORAGE를 select하게 합니다. tests/kernel/threads/tls를 실행해 구현을 검증합니다.
Device Drivers
Kernel 동작에 필요한 hardware device는 매우 적습니다. 이론적으로 system clock 없이도 실행할 수 있으므로 interrupt controller만 필수입니다. 하지만 sanity-check test suite 대부분을 사용하려면 system clock도 사실상 필요합니다. 두 device는 보통 architecture에 묶여 있어 architecture port에 포함합니다.
Interrupt Controllers
Interrupt controller와 interrupt 개념 자체는 architecture 사이에 큰 차이가 있습니다.
x86에는 IDT(Interrupt Descriptor Table)와 여러 interrupt controller가 있으며 IDT 안의 interrupt 위치가 priority를 결정합니다.
ARM Cortex-M에서는 NVIC(Nested Vectored Interrupt Controller)가 architecture 정의의 일부입니다. NVIC vector table과 별도인 IDT 형태의 table이 필요 없고, table 위치는 IRQ priority와 관계없으며 entry별 priority를 program할 수 있습니다.
ARCv2 interrupt unit도 architecture 정의의 일부라 NVIC와 비슷하지만 번호 mapping은 다릅니다. ARC는 exception과 interrupt 번호가 1:1이라 exception 1이 IRQ1이고 device IRQ는 16부터 시작합니다. ARM에서는 IRQ0이 exception 16이고 exception 1을 IRQ-15처럼 볼 수 있습니다.
이 차이 때문에 interrupt-controller 구현은 architecture 사이에서 공유할 부분이 거의 없습니다.
System Clock
x86 architecture에는 APIC timer와 HPET가 있고, ARM Cortex-M에는 SYSTICK exception이 있으며, ARCv2에는 timer0/1 device가 있습니다. Kernel timeout은 system-clock timer driver의 interrupt handler context에서 처리합니다.
Console Over Serial Line
필수는 아니지만 architecture port debug에 거의 필수처럼 유용한 device가 polling 방식의 output-only serial port driver입니다. printk와 printf console output을 이 port로 보냅니다.
CONFIG_RAM_CONSOLE을 사용하면 모든 출력을 debugger가 읽을 수 있는 RAM circular buffer로 보내므로 serial driver 없이도 대체할 수 있습니다.
Utility Libraries
Kernel이 의존하는 일부 function은 modern processor에서 몇 instruction 또는 lock-less 방식으로 구현할 수 있으므로 architecture port가 제공합니다.
- Atomic operator: architecture instruction이 있으면
CONFIG_ATOMIC_OPERATIONS_ARCH를 사용합니다. 없다면 non-atomic operation 전후를irq_lock()·irq_unlock()으로 감싼 generic C version을CONFIG_ATOMIC_OPERATIONS_C로 사용합니다. - 최하위·최상위 set bit 찾기: 전용 instruction이 없어도 generic C function으로 구현할 수 있습니다.
Compiler built-in을 사용할 수도 있지만 필요한 compiler barrier가 실제로 포함되는지 주의해야 합니다.
CPU Idling/Power Management
Kernel은 arch_cpu_idle()과 arch_cpu_atomic_idle() 두 function으로 CPU power management를 지원합니다.
arch_cpu_idle()은 interrupt가 unlock된 상태에서 x86 hlt, ARM wfi·wfe, ARC sleep 같은 architecture power-saving instruction을 호출하는 정도로 구현할 수 있습니다. Sleep 직전 interrupt를 받아도 상관없는 context에서 loop로 호출할 수 있습니다.
올바른 사용 사례는 두 가지입니다. Single-threaded system에서 initialization 뒤 실제 작업을 하지 않고 계속 idle loop에 머무는 유일한 thread, 또는 idle thread입니다.
arch_cpu_atomic_idle()은 interrupt 재활성화와 power-saving instruction 실행을 atomic하게 수행해야 합니다. 따라서 single-threaded system의 실제 application code에서도 사용할 수 있습니다.
일반적으로 CPU idling은 idle thread에 맡겨야 하지만 특수한 경우 application이 이 API를 쓸 수 있습니다. 두 function은 architecture에 반드시 존재해야 합니다. 최소 구현은 interrupt unlock 뒤 NOP이지만 실제 power-saving 구현을 강하게 권장합니다.
Fault Management
처리되지 않은 CPU exception이 발생하면 architecture code는 z_fatal_error()를 호출해야 합니다. 이 function은 architecture 독립 정보를 출력하고 k_sys_fatal_error()를 호출해 다음 policy를 결정합니다.
k_sys_fatal_error()는 application별 policy로 override할 수 있습니다. 기본처럼 interrupt를 lock하고 영원히 spin하거나, 지원한다면 system power를 끄는 방식도 가능합니다.
Toolchain and Linking
Build system에 toolchain 지원을 추가해야 합니다. `include/zephyr/toolchain/gcc.h`에는 architecture별 정의가 필요하므로 기존 지원 architecture의 정의를 참고합니다.
각 architecture는 자체 linker script도 필요합니다. 대부분의 section을 다른 architecture script에서 가져올 수 있어도 ARM의 SCB section이나 x86의 IDT section처럼 새 architecture에만 필요한 section이 있을 수 있습니다.
Memory Management and Memory Mapping
Target platform이 paging을 활성화하고 driver가 I/O region을 memory-map해야 한다면 CONFIG_MMU를 활성화하고 arch_mem_map(), arch_mem_unmap(), arch_page_phys_get() API를 구현합니다.
Stack Objects
Memory-protection hardware 유무는 stack object 생성 방식에 영향을 줍니다. 모든 architecture port는 CPU와 ABI requirement를 조합한 stack-pointer alignment를 ARCH_STACK_PTR_ALIGN으로 정의해야 합니다. 보통 4, 8, 16 byte처럼 작은 값입니다.
Thread stack은 두 종류입니다.
- Kernel stack:
K_KERNEL_STACK_DEFINE()계열 API로 정의합니다. Supervisor-mode kernel thread 또는 interrupt/exception handler stack으로 사용합니다. Alignment requirement가 훨씬 느슨하고 reserved data가 적으며 privilege-elevation stack 공간을 예약하지 않습니다. - Thread stack: 보통 더 많은 memory를 쓰지만 user-mode thread와 kernel-stack 용도를 모두 지원합니다.
CONFIG_USERSPACE가 꺼져 있으면 thread stack과 kernel stack은 같습니다.
Architecture layer는 stack object base alignment, thread stack buffer가 아닌 reserved data, user-mode용 stack-size rounding을 정의하는 macro를 추가할 수 있습니다. 정의가 없으면 다음 기본값을 사용합니다.
ARCH_KERNEL_STACK_RESERVED: reserved space 없음ARCH_THREAD_STACK_RESERVED: reserved space 없음ARCH_KERNEL_STACK_OBJ_ALIGN:ARCH_STACK_PTR_ALIGN에 정렬ARCH_THREAD_STACK_OBJ_ALIGN:ARCH_STACK_PTR_ALIGN에 정렬ARCH_THREAD_STACK_SIZE_ALIGN:ARCH_STACK_PTR_ALIGN까지 올림
모든 stack-creation macro는 이 정의를 바탕으로 만들어집니다.
Stack object는 configuration에 따라 일부 region 크기가 0일 수 있지만, 시작 부분의 reserved memory와 그 뒤 stack buffer라는 두 main part를 항상 갖습니다. 일부 경계는 연결된 thread object를 알아야 runtime에 결정할 수 있고 나머지는 build time에 계산할 수 있습니다.
ARM FPU처럼 architecture가 stack buffer에서 runtime reserved memory를 carve-out하거나 기존 reserved area를 보충해야 할 수 있습니다. Carve-out은 항상 thread.stack_info.start에 반영됩니다. thread.stack_info.start부터 thread.stack_info.size까지는 user-mode thread가 모두 접근할 수 있습니다. thread.stack_info.delta는 TLS와 ASLR random offset을 고려해 stack object 끝에서 initial stack pointer를 계산하는 offset입니다.
+---------------------+ <- thread.stack_obj
| Reserved Memory | } K_(THREAD|KERNEL)_STACK_RESERVED
+---------------------+
| Carved-out memory |
|.....................| <- thread.stack_info.start
| Unused stack buffer |
| |
|.....................| <- thread's current stack pointer
| Used stack buffer |
| |
|.....................| <- Initial stack pointer. Computable
| ASLR Random offset | with thread.stack_info.delta
+---------------------| <- thread.userspace_local_data
| Thread-local data |
+---------------------+ <- thread.stack_info.start + thread.stack_info.sizeK_(THREAD|KERNEL)_STACK_RESERVED
runtime 예약 영역
stack_info.start부터 현재 SP를 지나 사용 영역까지
stack_info.delta로 initial SP 계산
userspace_local_data부터 stack_info 끝까지
현재 Zephyr는 위쪽으로 자라는 stack을 지원하지 않습니다.
No Memory Protection
Memory protection을 사용하지 않는다면 기본값만으로 충분합니다.
HW-based stack overflow detection
이 option은 supervisor mode thread가 stack을 넘으면 hardware 기능으로 fatal error를 일으킵니다. Debug에는 유용하지만 overflow 뒤 system state가 온전하다고 신뢰할 수는 없습니다.
- Kernel critical section 안에서 overflow가 발생해 중요한 global data structure가 이미 손상됐을 수 있습니다.
- Guard memory region을 쓰는 system에서는 write가 guard를 뛰어넘어 인접 data를 먼저 손상한 뒤 hardware가 감지할 수 있습니다.
CONFIG_HW_STACK_PROTECTION을 활성화하려면 hardware 기반 overflow protection을 제공하고 CONFIG_ARCH_HAS_STACK_PROTECTION을 enable해야 합니다.
지원 방식은 전용 CPU 기능과 stack buffer 바로 앞의 read-only guard region 두 가지입니다.
CONFIG_HW_STACK_PROTECTION은 supervisor thread의 overflow만 잡습니다. User thread overflow 감지는 CONFIG_USERSPACE가 담당하며 서로 독립입니다. System call 처리 중 overflow도 supervisor-mode overflow에 포함되고 fatal error로 취급해야 하며 전체 kernel integrity를 보장할 수 없습니다. 반면 user-mode overflow는 kernel 관점에서 복구할 수 있고 별도 stack-protection configuration이 필요 없습니다.
CPU-based stack overflow detection
ARM SPLIM 같은 전용 CPU register로 supervisor-mode overflow를 감지한다면 기본 stack macro로 충분합니다.
Guard-based stack overflow detection
이 방식은 stack buffer 바로 앞에 write 시 exception을 만드는 memory-protection region을 둡니다. Reserved memory를 guard region으로 사용합니다.
ARCH_KERNEL_STACK_RESERVED는 memory-protection region의 최소 크기로 정의해야 합니다. 대부분 ARM CPU에서는 32 byte입니다. ARCH_KERNEL_STACK_OBJ_ALIGN도 그 region이 요구하는 alignment로 설정합니다.
MMU system은 guard용 RAM을 예약하지 말고 address space에 stack을 map할 때 바로 아래 virtual page를 non-present로 남겨야 합니다. 그래도 stack object 크기와 alignment는 page granularity에 맞아야 합니다.
+-----------------------------+ <- thread.stack_obj
| Guard reserved memory | } K_KERNEL_STACK_RESERVED
+-----------------------------+
| Guard carve-out |
|.............................| <- thread.stack_info.start
| Stack buffer |
. .K_KERNEL_STACK_RESERVED
필요한 경우 runtime 확장
stack_info.start 이후의 실제 stack
Kernel stack의 guard carve-out은 드물며 가능하면 피해야 합니다. 주로 두 경우에 필요합니다.
- 같은 stack을 user thread용으로 재사용할 수 있어 guard를 항상 예약하면 안 되는 경우입니다. Privilege-elevation stack이 stack object 안에 없을 때 해당합니다.
- Guard 크기가 context에 따라 달라지는 경우입니다. 일부 ARM CPU는 exception에서 lazy floating-point stacking을 하며 write 없이 SP를 크게 내릴 수 있어 최소 guard를 완전히 건너뛸 수 있습니다. Thread가 floating point를 사용할 때만 추가 memory를 carve-out하면 큰 guard를 항상 예약하지 않아도 됩니다.
User mode enabled
User mode를 활성화하면 요구사항 두 가지가 추가됩니다.
- User thread가 접근할 수 없는 고정 크기 privilege-mode stack을
CONFIG_PRIVILEGED_STACK_SIZE만큼 할당합니다. Kernel이 system call을 처리할 때 이 stack을 사용합니다. Stack guard를 쓴다면 앞에 guard를 둘 수 있어야 하고 필요한 carve-out도 지원해야 합니다. - Memory-protection hardware가
thread.stack_info가 추적하는 thread stack buffer를 정확히 덮는 region을 program할 수 있어야 합니다.ARCH_THREAD_STACK_SIZE_ADJUST()는 요청 크기를 region에 맞게 올리고ARCH_THREAD_STACK_OBJ_ALIGN()은 protection granularity에 맞춰야 합니다.
모든 memory region이 power-of-two 크기이고 자기 크기에 맞춰 정렬돼야 하는 오래된 MPU에서는 더 복잡합니다. 이 조건은 CONFIG_MPU_REQUIRES_POWER_OF_TWO_ALIGNMENT로 표시합니다.
thread.stack_info는 항상 stack object의 user-accessible 부분을 정확히 추적해야 하며 저장된 range로 user-access memory-protection region을 program할 수 있어야 합니다.
Non power-of-two memory region requirements
Power-of-two 제한이 없는 system은 K_THREAD_STACK_RESERVED가 정의하는 reserved area 안에 privilege-mode stack을 둘 수 있습니다.
+------------------------------+ <- thread.stack_obj
| Other platform data |
+------------------------------+
| Guard region (if enabled) |
+------------------------------+
| Guard carve-out (if needed) |
|..............................|
| Privilege elevation stack |
+------------------------------| <- thread.stack_obj +
| Stack buffer | K_THREAD_STACK_RESERVED =
. . thread.stack_info.start필요하면 guard 앞에 배치
thread 생성 때 read-only로 설정
system call용 supervisor stack
thread.stack_info.start부터 user access
Guard region과 필요한 carve-out은 thread 생성 시 read-only region으로 설정합니다.
- Supervisor thread라면 privilege-elevation region도 추가 stack memory입니다. Overflow는 결국 guard region에 닿습니다.
- User mode라면 stack buffer에만 user access를 허용합니다. User-mode overflow는 접근할 수 없는 privilege stack에 닿고, system call 처리 중 overflow는 guard region에 닿습니다.
MMU system은 physical guard를 두지 않습니다. Privilege-mode stack은 kernel memory, stack buffer는 user memory에 map하고 각각 아래에 non-present virtual guard page를 둡니다.
다른 platform data를 guard 앞에 저장할 수 있지만 thread.arch에 둘 수 있다면 강하게 권장하지 않습니다. ARCH_THREAD_STACK_RESERVED는 platform data, privilege stack, guard를 포함한 reserved region 전체 크기로 정의하고, 바로 뒤 stack buffer에 user access MPU region을 놓을 수 있게 크기를 맞춰야 합니다.
Power-of-two memory region requirements
Thread stack object는 memory에 효율적으로 배치하기 위해 같은 power-of-two 크기와 alignment를 사용하고 reserved memory를 두지 않아야 합니다. 따라서 guard는 stack object 안에서 완전히 carve-out하고 privilege-elevation stack은 다른 곳에 할당합니다.
ARCH_THREAD_STACK_SIZE_ADJUST()와 ARCH_THREAD_STACK_OBJ_ALIGN()은 모두 Z_POW2_CEIL()로 정의하고 K_THREAD_STACK_RESERVED는 0이어야 합니다.
CONFIG_GEN_PRIV_STACKS를 활성화하면 system에서 발견한 모든 thread stack마다 system call용 고정 크기 kernel stack을 생성합니다. Runtime에는 thread-stack address로 z_priv_stack_find()를 호출해 privilege stack address를 빠르게 찾습니다. 이 stack은 다른 kernel-only stack과 같은 배치를 사용합니다.
+-----------------------------+ <- z_priv_stack_find(thread.stack_obj)
| Reserved memory | } K_KERNEL_STACK_RESERVED
+-----------------------------+
| Guard carve-out (if needed) |
|.............................|
| Privilege elevation stack |
| |
+-----------------------------+ <- z_priv_stack_find(thread.stack_obj) +
K_KERNEL_STACK_RESERVED +
CONFIG_PRIVILEGED_STACK_SIZE
+-----------------------------+ <- thread.stack_obj
| MPU guard carve-out |
| (supervisor mode only) |
|.............................| <- thread.stack_info.start
| Stack buffer |
. .reserved memory + guard carve-out + privilege stack
thread stack address로 별도 privilege stack 검색
supervisor mode에서만 MPU guard carve-out
user mode 전환 시 object 전체를 사용
thread.stack_obj 안 guard carve-out은 supervisor mode thread에만 사용합니다. Thread가 user mode로 내려가면 guard가 없어지고 object 전체가 stack buffer가 되며 user thread가 모두 접근할 수 있도록 thread.stack_info도 갱신합니다.
User Mode Threads
User-mode thread를 지원하려면 kernel-to-architecture API를 구현하고 CONFIG_ARCH_HAS_USERSPACE를 enable해야 합니다.
arch_buffer_validate()는 current thread가 특정 memory region에 접근할 권한이 있는지 검사합니다.arch_user_mode_enter()는 supervisor thread를 되돌릴 수 없이 user privilege로 내리고 stack을 지웁니다.arch_syscall_oops()는 system-call parameter validation 실패 시 kernel oops를 만들되 user thread가 system call을 호출한 지점에서 난 것처럼 표시합니다.arch_syscall_invoke0()부터arch_syscall_invoke6()까지는 argument 수에 맞춰 system call을 호출하며 privilege elevation 때 모든 argument를 register로 전달해야 합니다.arch_is_user_context()는 CPU가 현재 user mode이면 0이 아닌 값을 반환합니다.arch_mem_domain_max_partitions_get()은 memory domain의 최대 region 수를 반환합니다. MMU는 사실상 무제한이고 MPU에는 제약이 있습니다.
Memory-domain API 호출 때 software management structure를 갱신하거나 다른 CPU의 hardware register를 바꿔야 하는 architecture는 CONFIG_ARCH_MEM_DOMAIN_SYNCHRONOUS_API를 select하고 arch_mem_domain_thread_add(), arch_mem_domain_thread_remove(), arch_mem_domain_partition_add(), arch_mem_domain_partition_remove()를 구현합니다. MMU에서는 흔하고 MPU에서는 드뭅니다.
API 구현 외에도 다음 작업이 필요합니다.
_new_thread가K_USERthread를 user mode로 시작해야 합니다.- Context switch 때 outgoing thread stack은 user mode가 접근하지 못하게, incoming thread stack은 접근할 수 있게 memory-protection hardware를 재설정합니다. 다른 thread stack을 훼손하지 못하게 하는 조치입니다.
- Context switch 때 outgoing·incoming thread의 memory domain도 전환합니다.
- Thread stack area에 user가 항상 접근할 수 없는 고정 크기 kernel stack을 포함합니다. 모든 system call을 처리할 만큼 커야 합니다.
- Software interrupt 또는 다른 privilege-elevation mechanism을 만들고
arch_syscall_invoke*macro와 연결합니다. System call에서는_k_syscall_table에서 handler를 찾고 잘못된 ID는K_SYSCALL_BADhandler로 보내며, 반환할 때 register state가 user mode로 새지 않게 합니다.
GDB Stub
새 architecture에서 remote-debugging GDB stub를 활성화하는 절차는 다음과 같습니다.
- Header와 context:
include/arch/<arch>/gdbstub.h를 만들고struct gdb_ctx를 정의합니다. GDB exception reason을 저장하는unsigned int exceptionmember는 필수이며z_gdb_main_loop()진입 전에 설정합니다. 필요한 member는 추가할 수 있고 struct pointer는 main loop와 다른 stub function으로 전달됩니다. - Main-loop 진입·종료: Breakpoint service가 interrupt 기반이면 ISR을 GDB main-loop entry로 구현합니다. Breakpoint가 없었던 것처럼 실행을 계속할 수 있도록 context를 저장·복원하고, 저장 뒤
z_gdb_main_loop()를 호출합니다. 호출 전gdb_ctx.exception을 설정합니다. - Stub function: 아래 architecture API를 구현합니다.
arch_gdb_init()은 context와 debug interrupt를 초기화하고 architecture별 방법으로 실행을 멈춰 boot 중 GDB가 연결할 수 있게 합니다.arch_gdb_continue()는 GDB의c또는continuecommand에서 실행을 계속합니다.arch_gdb_step()은si또는stepicommand에서 machine instruction 하나를 실행한 뒤 GDB prompt로 돌아갑니다.
GDB stub 자체가 target에서 실행되므로 hardware register 조작은 stub 실행을 망치지 않도록 cache에 적용해야 합니다. Context switch처럼 이전 thread의 register를 저장하고 cache copy만 수정한 뒤 원래 thread로 돌아가기 직전에 hardware register에 씁니다.
arch_gdb_reg_readall()은 GDB의 g/G packet에 들어갈 모든 register를 모읍니다. Packet format은 architecture별이며 길이가 틀리면 GDB가 debug session을 중단할 수 있습니다.arch_gdb_reg_writeall()은 GDB가 보낸 G-packet 값으로 register cache를 채웁니다.arch_gdb_reg_readone()은 register 하나를 읽어 GDB로 보냅니다.arch_gdb_reg_writeone()은 GDB에서 받은 register 하나의 값을 씁니다.
arch_gdb_add_breakpoint()와 arch_gdb_remove_breakpoint()는 breakpoint를 다룹니다. GDB는 instruction을 software breakpoint 또는 trap instruction으로 바꾸고 도달 뒤 원래 memory를 복원할 수 있으므로 type 0 software breakpoint는 보통 architecture code가 따로 처리하지 않아도 됩니다.
Runtime에 수정할 수 없는 ROM·flash code에는 type 1 hardware breakpoint가 필요합니다. 설정 방법은 architecture datasheet를 따릅니다. Architecture가 hardware breakpoint를 지원하지 않으면 두 function을 구현하지 않아도 되고 GDB가 software breakpoint에 의존합니다.
일부 memory region에 접근할 수 없는 architecture는 접근 가능한 영역을 나타내는 struct gdb_mem_region 배열 gdb_mem_region_array를 정의합니다.
gdb_mem_region.start와end는 region 시작과 끝입니다.attributes는GDB_MEM_REGION_RO또는GDB_MEM_REGION_RWpermission입니다.alignment는 read/write alignment이고 제한 없이 byte 단위 access가 가능하면 0입니다.
API Reference
Timing
arch-timing Doxygen group이 architecture timing API를 제공합니다.
Threads
arch-threads와 arch-tls Doxygen group이 thread·TLS architecture API를 제공합니다.
Power Management
arch-pm Doxygen group이 architecture power-management API를 제공합니다.
Symmetric Multi-Processing
arch-smp Doxygen group이 SMP architecture API를 제공합니다.
Interrupts
arch-irq Doxygen group이 architecture interrupt API를 제공합니다.
Userspace
arch-userspace Doxygen group이 userspace architecture API를 제공합니다.
Memory Management
arch-mmu Doxygen group이 architecture MMU API를 제공합니다.
Miscellaneous Architecture APIs
arch-misc Doxygen group이 그 밖의 architecture API를 제공합니다.
GDB Stub APIs
arch-gdbstub Doxygen group이 GDB-stub architecture API를 제공합니다.
Source
출처
영어 원문에는 문단, 문서 지시문, 표, 코드, 코드 심벌, 경로를 그대로 보존했습니다.