start_kernel
Su 4.2, start_kernel
da init/main.c
è un processo di inizializzazione considerevole e potrebbe essere paragonato a un main
funzione.
È il primo codice indipendente dall'architettura da eseguire e imposta gran parte del kernel. Proprio come main
, start_kernel
è preceduto da un codice di configurazione di livello inferiore (fatto nel crt*
oggetti in userland main
), dopo di che viene eseguito il codice C generico "principale".
Come start_kernel
viene chiamato in x86_64
arch/x86/kernel/vmlinux.lds.S
, uno script linker, imposta:
ENTRY(phys_startup_64)
e
phys_startup_64 = startup_64 - LOAD_OFFSET;
e:
#define LOAD_OFFSET __START_KERNEL_map
arch/x86/include/asm/page_64_types.h
definisce __START_KERNEL_map
come:
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
che è l'indirizzo di ingresso del kernel. TODO come si raggiunge esattamente quell'indirizzo? Devo capire l'interfaccia che Linux espone ai bootloader.
arch/x86/kernel/vmlinux.lds.S
imposta la primissima sezione del bootloader come:
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
/* bootstrapping code */
HEAD_TEXT
include/asm-generic/vmlinux.lds.h
definisce HEAD_TEXT
:
#define HEAD_TEXT *(.head.text)
arch/x86/kernel/head_64.S
definisce startup_64
. Questo è il primissimo codice del kernel x86 che viene eseguito. Fa molto di configurazione di basso livello, inclusa la segmentazione e il paging.
Questa è quindi la prima cosa che viene eseguita perché il file inizia con:
.text
__HEAD
.code64
.globl startup_64
e include/linux/init.h
definisce __HEAD
come:
#define __HEAD .section ".head.text","ax"
così come la prima cosa dello script del linker.
Alla fine chiama x86_64_start_kernel
un po' goffamente con e lretq
:
movq initial_code(%rip),%rax
pushq $0 # fake return address to stop unwinder
pushq $__KERNEL_CS # set correct cs
pushq %rax # target address in negative space
lretq
e:
.balign 8
GLOBAL(initial_code)
.quad x86_64_start_kernel
arch/x86/kernel/head64.c
definisce x86_64_start_kernel
che chiama x86_64_start_reservations
che chiama start_kernel
.
punto di ingresso arm64
Il primissimo arm64 che gira su un kernel v5.7 non compresso è definito su https://github.com/cirosantilli/linux/blob/v5.7/arch/arm64/kernel/head.S#L72 quindi sia il add x13, x18, #0x16
o b stext
a seconda di CONFIG_EFI
:
__HEAD
_head:
/*
* DO NOT MODIFY. Image header expected by Linux boot-loaders.
*/
#ifdef CONFIG_EFI
/*
* This add instruction has no meaningful effect except that
* its opcode forms the magic "MZ" signature required by UEFI.
*/
add x13, x18, #0x16
b stext
#else
b stext // branch to kernel start, magic
.long 0 // reserved
#endif
le64sym _kernel_offset_le // Image load offset from start of RAM, little-endian
le64sym _kernel_size_le // Effective size of kernel image, little-endian
le64sym _kernel_flags_le // Informative flags, little-endian
.quad 0 // reserved
.quad 0 // reserved
.quad 0 // reserved
.ascii ARM64_IMAGE_MAGIC // Magic number
#ifdef CONFIG_EFI
.long pe_header - _head // Offset to the PE header.
Questo è anche il primissimo byte di un'immagine del kernel non compressa.
Entrambi questi casi passano a stext
che avvia l'azione "reale".
Come accennato nel commento, queste due istruzioni sono i primi 64 byte di un'intestazione documentata descritta in:https://github.com/cirosantilli/linux/blob/v5.7/Documentation/arm64/booting.rst#4-call -l'immagine-del-kernel
arm64 prima istruzione abilitata per MMU:__primary_switched
Penso che sia __primary_switched
in testa.S:
/*
* The following fragment of code is executed with the MMU enabled.
*
* x0 = __PHYS_OFFSET
*/
__primary_switched:
A questo punto, il kernel sembra creare tabelle di pagine + forse riposizionarsi in modo tale che gli indirizzi del PC corrispondano ai simboli del file ELF di vmlinux. Pertanto a questo punto dovresti essere in grado di vedere nomi di funzioni significative in GDB senza ulteriori magie.
punto di ingresso della CPU secondaria arm64
secondary_holding_pen
definito su:https://github.com/cirosantilli/linux/blob/v5.7/arch/arm64/kernel/head.S#L691
Procedura di accesso ulteriormente descritta su:https://github.com/cirosantilli/linux/blob/v5.7/arch/arm64/kernel/head.S#L691
Con main()
probabilmente intendi cosa main()
è per un programma, vale a dire il suo "punto di ingresso".
Per un modulo che è init_module()
.
Dalla seconda edizione di Linux Device Driver:
Mentre un'applicazione esegue una singola attività dall'inizio alla fine, un modulo si registra per soddisfare le richieste future e la sua funzione "principale" termina immediatamente. In altre parole, il compito della funzione init_module (il punto di ingresso del modulo) è preparare la successiva invocazione delle funzioni del modulo; è come se il modulo dicesse:"Eccomi, e questo è quello che posso fare". Il secondo punto di ingresso di un modulo, cleanup_module, viene richiamato appena prima che il modulo venga scaricato. Dovrebbe dire al kernel:"Non ci sono più; non chiedermi di fare nient'altro".
Fondamentalmente, non c'è niente di speciale nel fatto che una routine si chiami main()
. Come accennato in precedenza, main()
funge da punto di ingresso per un modulo di caricamento eseguibile. Tuttavia, è possibile definire diversi punti di ingresso per un modulo di caricamento. Infatti, puoi definire più di un punto di ingresso, ad esempio fare riferimento alla tua dll preferita.
Dal punto di vista del sistema operativo (OS), tutto ciò di cui ha veramente bisogno è l'indirizzo del punto di ingresso del codice che funzionerà come driver di dispositivo. Il sistema operativo passerà il controllo a quel punto di ingresso quando il driver del dispositivo è tenuto a eseguire l'I/O al dispositivo.
Un programmatore di sistema definisce (ogni sistema operativo ha il proprio metodo) la connessione tra un dispositivo, un modulo di caricamento che funge da driver del dispositivo e il nome del punto di ingresso nel modulo di caricamento.
Ogni sistema operativo ha il proprio kernel (ovviamente) e alcuni potrebbero/forse iniziare con main()
ma sarei sorpreso di trovare un kernel che usa main()
altro che in uno semplice, come UNIX! Nel momento in cui stai scrivendo il codice del kernel hai superato da tempo il requisito di nominare ogni modulo che scrivi come main()
.
Spero che questo aiuti?
Ho trovato questo frammento di codice dal kernel per Unix versione 6. Come puoi vedere main()
è solo un altro programma, che cerca di iniziare!
main()
{
extern schar;
register i, *p;
/*
* zero and free all of core
*/
updlock = 0;
i = *ka6 + USIZE;
UISD->r[0] = 077406;
for(;;) {
if(fuibyte(0) < 0) break;
clearsig(i);
maxmem++;
mfree(coremap, 1, i);
i++;
}
if(cputype == 70)
for(i=0; i<62; i=+2) {
UBMAP->r[i] = i<<12;
UBMAP->r[i+1] = 0;
}
// etc. etc. etc.
Diversi modi per vederlo:
-
I driver di dispositivo non sono programmi. Sono moduli che vengono caricati in un altro programma (il kernel). In quanto tali, non hanno un
main()
funzione. -
Il fatto che tutti i programmi devono avere un
main()
la funzione è valida solo per le applicazioni in spazio utente. Non si applica al kernel, né ai driver di dispositivo.