Continue architecture-specific boot-time initializations
Kernel initialization. Part 5.
Continue of architecture-specific initialization
In the previous part, we stopped at the initialization of an architecture-specific stuff from the setup_arch function and now we will continue with it. As we reserved memory for the initrd, next step is the olpc_ofw_detect
which detects One Laptop Per Child support. We will not consider platform related stuff in this book and will skip functions related with it. So let's go ahead. The next step is the early_trap_init
function. This function initializes debug (#DB
- raised when the TF
flag of rflags is set) and int3
(#BP
) interrupts gate. If you don't know anything about interrupts, you can read about it in the Early interrupt and exception handling. In x86
architecture INT
, INTO
and INT3
are special instructions which allow a task to explicitly call an interrupt handler. The INT3
instruction calls the breakpoint (#BP
) handler. You may remember, we already saw it in the part about interrupts: and exceptions:
Debug interrupt #DB
is the primary method of invoking debuggers. early_trap_init
defined in the arch/x86/kernel/traps.c. This functions sets #DB
and #BP
handlers and reloads IDT:
We already saw implementation of the set_intr_gate
in the previous part about interrupts. Here are two similar functions set_intr_gate_ist
and set_system_intr_gate_ist
. Both of these two functions take three parameters:
number of the interrupt;
base address of the interrupt/exception handler;
third parameter is -
Interrupt Stack Table
.IST
is a new mechanism in thex86_64
and part of the TSS. Every active thread in kernel mode has own kernel stack which is16
kilobytes. While a thread in user space, this kernel stack is empty.
In addition to per-thread stacks, there are a couple of specialized stacks associated with each CPU. All about these stack you can read in the Linux kernel documentation - Kernel stacks. x86_64
provides feature which allows to switch to a new special
stack for during any events as non-maskable interrupt and etc... And the name of this feature is - Interrupt Stack Table
. There can be up to 7 IST
entries per CPU and every entry points to the dedicated stack. In our case this is DEBUG_STACK
.
set_intr_gate_ist
and set_system_intr_gate_ist
work by the same principle as set_intr_gate
with only one difference. Both of these functions checks interrupt number and call _set_gate
inside:
as set_intr_gate
does this. But set_intr_gate
calls _set_gate
with dpl - 0, and ist - 0, but set_intr_gate_ist
and set_system_intr_gate_ist
sets ist
as DEBUG_STACK
and set_system_intr_gate_ist
sets dpl
as 0x3
which is the lowest privilege. When an interrupt occurs and the hardware loads such a descriptor, then hardware automatically sets the new stack pointer based on the IST value, then invokes the interrupt handler. All of the special kernel stacks will be set in the cpu_init
function (we will see it later).
As #DB
and #BP
gates written to the idt_descr
, we reload IDT
table with load_idt
which just call ldtr
instruction. Now let's look on interrupt handlers and will try to understand how they works. Of course, I can't cover all interrupt handlers in this book and I do not see the point in this. It is very interesting to delve in the Linux kernel source code, so we will see how debug
handler implemented in this part, and understand how other interrupt handlers are implemented will be your task.
#DB handler
As you can read above, we passed address of the #DB
handler as &debug
in the set_intr_gate_ist
. lxr.free-electrons.com is a great resource for searching identifiers in the Linux kernel source code, but unfortunately you will not find debug
handler with it. All of you can find, it is debug
definition in the arch/x86/include/asm/traps.h:
We can see asmlinkage
attribute which tells to us that debug
is function written with assembly. Yeah, again and again assembly :). Implementation of the #DB
handler as other handlers is in this arch/x86/entry/entry_64.S and defined with the idtentry
assembly macro:
idtentry
is a macro which defines an interrupt/exception entry point. As you can see it takes five arguments:
name of the interrupt entry point;
name of the interrupt handler;
has interrupt error code or not;
paranoid - if this parameter = 1, switch to special stack (read above);
shift_ist - stack to switch during interrupt.
Now let's look on idtentry
macro implementation. This macro defined in the same assembly file and defines debug
function with the ENTRY
macro. For the start idtentry
macro checks that given parameters are correct in case if need to switch to the special stack. In the next step it checks that give interrupt returns error code. If interrupt does not return error code (in our case #DB
does not return error code), it calls INTR_FRAME
or XCPT_FRAME
if interrupt has error code. Both of these macros XCPT_FRAME
and INTR_FRAME
do nothing and need only for the building initial frame state for interrupts. They uses CFI
directives and used for debugging. More info you can find in the CFI directives. As comment from the arch/x86/kernel/entry/entry_64.S says: CFI macros are used to generate dwarf2 unwind information for better backtraces. They don't change any code.
so we will ignore them.
You can remember from the previous part about early interrupts/exceptions handling that after interrupt occurs, current stack will have following format:
The next two macro from the idtentry
implementation are:
First ASM_CLAC
macro depends on CONFIG_X86_SMAP
configuration option and need for security reason, more about it you can read here. The second PARAVIRT_ADJUST_EXCEPTION_FRAME
macro is for handling handle Xen-type-exceptions (this chapter about kernel initialization and we will not consider virtualization stuff here).
The next piece of code checks if interrupt has error code or not and pushes $-1
which is 0xffffffffffffffff
on x86_64
on the stack if not:
We need to do it as dummy
error code for stack consistency for all interrupts. In the next step we subtract from the stack pointer $ORIG_RAX-R15
:
where ORIRG_RAX
, R15
and other macros defined in the arch/x86/entry/calling.h and ORIG_RAX-R15
is 120 bytes. General purpose registers will occupy these 120 bytes because we need to store all registers on the stack during interrupt handling. After we set stack for general purpose registers, the next step is checking that interrupt came from userspace with:
Here we checks first and second bits in the CS
. You can remember that CS
register contains segment selector where first two bits are RPL
. All privilege levels are integers in the range 0–3, where the lowest number corresponds to the highest privilege. So if interrupt came from the kernel mode we call save_paranoid
or jump on label 1
if not. In the save_paranoid
we store all general purpose registers on the stack and switch user gs
on kernel gs
if need:
In the next steps we put pt_regs
pointer to the rdi
, save error code in the rsi
if it has and call interrupt handler which is - do_debug
in our case from the arch/x86/kernel/traps.c. do_debug
like other handlers takes two parameters:
pt_regs - is a structure which presents set of CPU registers which are saved in the process' memory region;
error code - error code of interrupt.
After interrupt handler finished its work, calls paranoid_exit
which restores stack, switch on userspace if interrupt came from there and calls iret
. That's all. Of course it is not all :), but we will see more deeply in the separate chapter about interrupts.
This is general view of the idtentry
macro for #DB
interrupt. All interrupts are similar to this implementation and defined with idtentry too. After early_trap_init
finished its work, the next function is early_cpu_init
. This function defined in the arch/x86/kernel/cpu/common.c and collects information about CPU and its vendor.
Early ioremap initialization
The next step is initialization of early ioremap
. In general there are two ways to communicate with devices:
I/O Ports;
Device memory.
We already saw first method (outb/inb
instructions) in the part about Linux kernel booting process. The second method is to map I/O physical addresses to virtual addresses. When a physical address is accessed by the CPU, it may refer to a portion of physical RAM which can be mapped on memory of the I/O device. So ioremap
used to map device memory into kernel address space.
As I wrote above next function is the early_ioremap_init
which re-maps I/O memory to kernel address space so it can access it. We need to initialize early ioremap for early initialization code which needs to temporarily map I/O or memory regions before the normal mapping functions like ioremap
are available. Implementation of this function is in the arch/x86/mm/ioremap.c. At the start of the early_ioremap_init
we can see definition of the pmd
pointer with pmd_t
type (which presents page middle directory entry typedef struct { pmdval_t pmd; } pmd_t;
where pmdval_t
is unsigned long
) and make a check that fixmap
aligned in a correct way:
fixmap
- is fixed virtual address mappings which extends from FIXADDR_START
to FIXADDR_TOP
. Fixed virtual addresses are needed for subsystems that need to know the virtual address at compile time. After the check early_ioremap_init
makes a call of the early_ioremap_setup
function from the mm/early_ioremap.c. early_ioremap_setup
fills slot_virt
array of the unsigned long
with virtual addresses with 512 temporary boot-time fix-mappings:
After this we get page middle directory entry for the FIX_BTMAP_BEGIN
and put to the pmd
variable, fills bm_pte
with zeros which is boot time page tables and call pmd_populate_kernel
function for setting given page table entry in the given page middle directory:
That's all for this. If you feeling puzzled, don't worry. There is special part about ioremap
and fixmaps
in the Linux Kernel Memory Management. Part 2 chapter.
Obtaining major and minor numbers for the root device
After early ioremap
was initialized, you can see the following code:
This code obtains major and minor numbers for the root device where initrd
will be mounted later in the do_mount_root
function. Major number of the device identifies a driver associated with the device. Minor number referred on the device controlled by driver. Note that old_decode_dev
takes one parameter from the boot_params_structure
. As we can read from the x86 Linux kernel boot protocol:
Now let's try to understand what old_decode_dev
does. Actually it just calls MKDEV
inside which generates dev_t
from the give major and minor numbers. It's implementation is pretty simple:
where dev_t
is a kernel data type to present major/minor number pair. But what's the strange old_
prefix? For historical reasons, there are two ways of managing the major and minor numbers of a device. In the first way major and minor numbers occupied 2 bytes. You can see it in the previous code: 8 bit for major number and 8 bit for minor number. But there is a problem: only 256 major numbers and 256 minor numbers are possible. So 16-bit integer was replaced by 32-bit integer where 12 bits reserved for major number and 20 bits for minor. You can see this in the new_decode_dev
implementation:
After calculation we will get 0xfff
or 12 bits for major
if it is 0xffffffff
and 0xfffff
or 20 bits for minor
. So in the end of execution of the old_decode_dev
we will get major and minor numbers for the root device in ROOT_DEV
.
Memory map setup
The next point is the setup of the memory map with the call of the setup_memory_map
function. But before this we setup different parameters as information about a screen (current row and column, video page and etc... (you can read about it in the Video mode initialization and transition to protected mode)), Extended display identification data, video mode, bootloader_type and etc...:
All of these parameters we got during boot time and stored in the boot_params
structure. After this we need to setup the end of the I/O memory. As you know one of the main purposes of the kernel is resource management. And one of the resource is memory. As we already know there are two ways to communicate with devices are I/O ports and device memory. All information about registered resources are available through:
/proc/ioports - provides a list of currently registered port regions used for input or output communication with a device;
/proc/iomem - provides current map of the system's memory for each physical device.
At the moment we are interested in /proc/iomem
:
As you can see range of addresses are shown in hexadecimal notation with its owner. Linux kernel provides API for managing any resources in a general way. Global resources (for example PICs or I/O ports) can be divided into subsets - relating to any hardware bus slot. The main structure resource
:
presents abstraction for a tree-like subset of system resources. This structure provides range of addresses from start
to end
(resource_size_t
is phys_addr_t
or u64
for x86_64
) which a resource covers, name
of a resource (you see these names in the /proc/iomem
output) and flags
of a resource (All resources flags defined in the include/linux/ioport.h). The last are three pointers to the resource
structure. These pointers enable a tree-like structure:
Every subset of resources has root range resources. For iomem
it is iomem_resource
which defined as:
TODO EXPORT_SYMBOL
iomem_resource
defines root addresses range for io memory with PCI mem
name and IORESOURCE_MEM
(0x00000200
) as flags. As i wrote above our current point is setup the end address of the iomem
. We will do it with:
Here we shift 1
on boot_cpu_data.x86_phys_bits
. boot_cpu_data
is cpuinfo_x86
structure which we filled during execution of the early_cpu_init
. As you can understand from the name of the x86_phys_bits
field, it presents maximum bits amount of the maximum physical address in the system. Note also that iomem_resource
is passed to the EXPORT_SYMBOL
macro. This macro exports the given symbol (iomem_resource
in our case) for dynamic linking or in other words it makes a symbol accessible to dynamically loaded modules.
After we set the end address of the root iomem
resource address range, as I wrote above the next step will be setup of the memory map. It will be produced with the call of the setup_ memory_map
function:
First of all we call look here the call of the x86_init.resources.memory_setup
. x86_init
is a x86_init_ops
structure which presents platform specific setup functions as resources initialization, pci initialization and etc... initialization of the x86_init
is in the arch/x86/kernel/x86_init.c. I will not give here the full description because it is very long, but only one part which interests us for now:
As we can see here memory_setup
field is default_machine_specific_memory_setup
where we get the number of the e820 entries which we collected in the boot time, sanitize the BIOS e820 map and fill e820map
structure with the memory regions. As all regions are collected, print of all regions with printk. You can find this print if you execute dmesg
command and you can see something like this:
Copying of the BIOS Enhanced Disk Device information
The next two steps is parsing of the setup_data
with parse_setup_data
function and copying BIOS EDD to the safe place. setup_data
is a field from the kernel boot header and as we can read from the x86
boot protocol:
It used for storing setup information for different types as device tree blob, EFI setup data and etc... In the second step we copy BIOS EDD information from the boot_params
structure that we collected in the arch/x86/boot/edd.c to the edd
structure:
Memory descriptor initialization
The next step is initialization of the memory descriptor of the init process. As you already can know every process has its own address space. This address space presented with special data structure which called memory descriptor
. Directly in the Linux kernel source code memory descriptor presented with mm_struct
structure. mm_struct
contains many different fields related with the process address space as start/end address of the kernel code/data, start/end of the brk, number of memory areas, list of memory areas and etc... This structure defined in the include/linux/mm_types.h. As every process has its own memory descriptor, task_struct
structure contains it in the mm
and active_mm
field. And our first init
process has it too. You can remember that we saw the part of initialization of the init task_struct
with INIT_TASK
macro in the previous part:
mm
points to the process address space and active_mm
points to the active address space if process has no address space such as kernel threads (more about it you can read in the documentation). Now we fill memory descriptor of the initial process:
with the kernel's text, data and brk. init_mm
is the memory descriptor of the initial process and defined as:
where mm_rb
is a red-black tree of the virtual memory areas, pgd
is a pointer to the page global directory, mm_users
is address space users, mm_count
is primary usage counter and mmap_sem
is memory area semaphore. After we setup memory descriptor of the initial process, next step is initialization of the Intel Memory Protection Extensions with mpx_mm_init
. The next step is initialization of the code/data/bss resources with:
We already know a little about resource
structure (read above). Here we fill code/data/bss resources with their physical addresses. You can see it in the /proc/iomem
:
All of these structures are defined in the arch/x86/kernel/setup.c and look like typical resource initialization:
The last step which we will cover in this part will be NX
configuration. NX-bit
or no execute bit is 63-bit in the page directory entry which controls the ability to execute code from all physical pages mapped by the table entry. This bit can only be used/set when the no-execute
page-protection mechanism is enabled by the setting EFER.NXE
to 1. In the x86_configure_nx
function we check that CPU has support of NX-bit
and it does not disabled. After the check we fill __supported_pte_mask
depend on it:
Conclusion
It is the end of the fifth part about Linux kernel initialization process. In this part we continued to dive in the setup_arch
function which makes initialization of architecture-specific stuff. It was long part, but we are not finished with it. As I already wrote, the setup_arch
is big function, and I am really not sure that we will cover all of it even in the next part. There were some new interesting concepts in this part like Fix-mapped
addresses, ioremap and etc... Don't worry if they are unclear for you. There is a special part about these concepts - Linux kernel memory management Part 2.. In the next part we will continue with the initialization of the architecture-specific stuff and will see parsing of the early kernel parameters, early dump of the pci devices, Desktop Management Interface
scanning and many many more.
If you have any questions or suggestions write me a comment or ping me at twitter.
Please note that English is not my first language, And I am really sorry for any inconvenience. If you find any mistakes please send me PR to linux-insides.
Links
Last updated