Куда именно выполнение передаётся загрузчиком, т.е. что является самой точкой входа в ядро. Давайте взглянем на команду, которая компонует ядро:
sys/conf/Makefile.i386: ld -elf -Bdynamic -T /usr/src/sys/conf/ldscript.i386 -export-dynamic \ -dynamic-linker /red/herring -o kernel -X locore.o \ <lots of kernel .o files>
Несколько интересных вещей можно заметить в данной строке. Во-первых, ядро - это динамически скомпонованный двоичный файл формата ELF, однако динамическим компоновщиком для ядра является определённо фиктивный файл /red/herring. Во-вторых, просмотр файла sys/conf/ldscript.i386 даёт представление о том, с какими опциями используется ld при компиляции ядра. Одна из первых строк файла
sys/conf/ldscript.i386: ENTRY(btext)
говорит о том, что точка входа в ядро - символ `btext'. Данный символ определён в locore.s:
sys/i386/i386/locore.s: .text /********************************************************************** * * This is where the bootblocks start us, set the ball rolling... * */ NON_GPROF_ENTRY(btext)
Первое, что происходит - это установка регистра EFLAGS в предопределённое значение 0x00000002, затем инициализируются все сегментные регистры:
sys/i386/i386/locore.s /* Don't trust what the BIOS gives for eflags. */ pushl $PSL_KERNEL popfl /* * Don't trust what the BIOS gives for %fs and %gs. Trust the bootstrap * to set %cs, %ds, %es and %ss. */ mov %ds, %ax mov %ax, %fs mov %ax, %gs
btext вызывает процедуры recover_bootinfo()
, identify_cpu()
, create_pagetables()
, которые также определены в locore.s. Далее следует описание того, что они делают:
recover_bootinfo |
Эта процедура производит синтаксический разбор параметров, переданных ядру во время начальной загрузки. Ядро могло быть загружено тремя способами: загрузчиком, описанным выше, старыми загрузочными блоками диска и старой процедурой бездисковой загрузки. Эта функция определяет метод загрузки и сохраняет структуру struct bootinfo в памяти ядра. |
identify_cpu |
Эта функция пытается определить, на каком процессоре работает и сохраняет найденное
значение в переменной _cpu . |
create_pagetables |
Эта функция выделяет память под и заполняет Каталог Таблиц Страниц (Page Table Directory) в вершине области памяти ядра. |
Следующие шаги - включение VME, если процессор его поддерживает:
testl $CPUID_VME, R(_cpu_feature) jz 1f movl %cr4, %eax orl $CR4_VME, %eax movl %eax, %cr4
Затем включение страничного режима:
/* Now enable paging */ movl R(_IdlePTD), %eax movl %eax,%cr3 /* load ptd addr into mmu */ movl %cr0,%eax /* get control word */ orl $CR0_PE|CR0_PG,%eax /* enable paging */ movl %eax,%cr0 /* and let's page NOW! */
Следующие три строчки кода появляются из-за включения страничного режима, потому как для продолжения выполнения в виртуальном адресном пространстве необходим переход:
pushl $begin /* jump to high virtualized address */ ret /* now running relocated at KERNBASE where the system is linked to run */ begin:
Вызывается функция init386()
с указателем на первую
свободную физическую страницу, после чего происходит вызов mi_startup()
. init386
является
архитектурно-зависимой функцией инициализации, а mi_startup()
- архитектурно-независимой (префикс 'mi_' означает
машинно-независимый (Machine Independent)). Ядро никогда не возвращается из mi_startup()
, и вызвав её, оно заканчивает загрузку:
sys/i386/i386/locore.s: movl physfree, %esi pushl %esi /* value of first for init386(first) */ call _init386 /* wire 386 chip for unix operation */ call _mi_startup /* autoconfiguration, mountroot etc */ hlt /* never returns to here */
init386()
Функция init386()
определена в файле sys/i386/i386/machdep.c. Она выполняет низкоуровневую, специфичную
для i386 инициализацию. Переключение в защищённый режим было выполнено загрузчиком.
Загрузчик создал самую первую задачу, в котором ядро продолжает выполняться. Перед тем,
как обратиться к коду, я перечислю задачи, которые процессор должен завершить для
инициализации выполнения в защищённом режиме:
Инициализировать настраиваемые ядром параметры, переданные ему программой загрузки.
Подготовить GDT.
Подготовить IDT.
Инициализировать системную консоль.
Инициализировать DDB, если он включён в ядро.
Инициализировать TSS.
Подготовить LDT.
Установить pcb для proc0.
init386()
первым делом инициализирует настраиваемые
параметры, переданные из начальной загрузки. Это осуществляется путём установки указателя
на окружение (environment pointer, envp) и вызовом init_param1()
. Указатель envp был передан загрузчиком в структуре
bootinfo:
sys/i386/i386/machdep.c: kern_envp = (caddr_t)bootinfo.bi_envp + KERNBASE; /* Init basic tunables, hz etc */ init_param1();
Функция init_param1()
определена в файле sys/kern/subr_param.c. В этом файле определены некоторые переменные
sysctl и две функции: init_param1()
и init_param2()
, которые вызываются из init386()
:
sys/kern/subr_param.c hz = HZ; TUNABLE_INT_FETCH("kern.hz", &hz);
TUNABLE_<typename>_FETCH используется для получения значения из окружения:
/usr/src/sys/sys/kernel.h #define TUNABLE_INT_FETCH(path, var) getenv_int((path), (var))
Sysctl kern.hz - это тик системного таймера. Этот и другие
sysctl устанавливаются в init_param1()
: kern.maxswzone, kern.maxbcache, kern.maxtsiz, kern.dfldsiz, kern.dflssiz,
kern.maxssiz, kern.sgrowsiz.
Далее init386()
подготавливает глобальную таблицу
дескрипторов (GDT). Каждая задача, выполняющаяся на процессоре архитектуры x86, работает
в своём собственном виртуальном адресном пространстве, и это пространство адресуется
парой сегмент:смещение. Пусть, для примера, текущая инструкция, которая должна быть
выполнена процессором, располагается по адресу CS:EIP, тогда линейным виртуальным адресом
этой инструкции будет ''виртуальный адрес сегмента когда CS'' + EIP. Для удобства будем
считать, что сегменты начинаются с виртуального адреса 0 и заканчиваются на границе 4Гб.
Таким образом, линейный виртуальный адрес инструкции для этого примера просто будет равен
значению регистра EIP. Сегментные регистры, такие как CS, DS и другие являются
селекторами, т.е. индексами в GDT (если быть более точными, сам индекс не является
селектором, он является полем INDEX селектора). GDT в FreeBSD содержит дескрипторы 15
селекторов для каждого процессора:
sys/i386/i386/machdep.c: union descriptor gdt[NGDT * MAXCPU]; /* global descriptor table */ sys/i386/include/segments.h: /* * Entries in the Global Descriptor Table (GDT) */ #define GNULL_SEL 0 /* Null Descriptor */ #define GCODE_SEL 1 /* Kernel Code Descriptor */ #define GDATA_SEL 2 /* Kernel Data Descriptor */ #define GPRIV_SEL 3 /* SMP Per-Processor Private Data */ #define GPROC0_SEL 4 /* Task state process slot zero and up */ #define GLDT_SEL 5 /* LDT - eventually one per process */ #define GUSERLDT_SEL 6 /* User LDT */ #define GTGATE_SEL 7 /* Process task switch gate */ #define GBIOSLOWMEM_SEL 8 /* BIOS low memory access (must be entry 8) */ #define GPANIC_SEL 9 /* Task state to consider panic from */ #define GBIOSCODE32_SEL 10 /* BIOS interface (32bit Code) */ #define GBIOSCODE16_SEL 11 /* BIOS interface (16bit Code) */ #define GBIOSDATA_SEL 12 /* BIOS interface (Data) */ #define GBIOSUTIL_SEL 13 /* BIOS interface (Utility) */ #define GBIOSARGS_SEL 14 /* BIOS interface (Arguments) */
Заметьте, что эти #define сами не являются селекторами, а только полем INDEX селектора, так что они в действительности - индексы GDT. К примеру, действительный селектор для кода ядра (GCODE_SEL) имеет значение 0x08.
На следующем шаге инициализируется таблица дескрипторов прерываний (Interrupt Description Table, IDT). На эту таблицу ссылается процессор, когда возникают программные или аппаратные прерывания. Например, для осуществления системного вызова пользовательское приложение вызывает инструкцию INT 0x80. Это программное прерывание, поэтому процессор ищет запись с индексом 0x80 в IDT. Эта запись указывает на процедуру обработки прерывания, которая в нашем примере является шлюзом системных вызовов ядра. IDT может иметь максимум 256 (0x100) записей. Ядро располагает (allocates) NIDT записей для IDT, где NIDT имеет значение не более 256:
sys/i386/i386/machdep.c: static struct gate_descriptor idt0[NIDT]; struct gate_descriptor *idt = &idt0[0]; /* interrupt descriptor table */
Для каждого прерывания устанавливается соответствующий обработчик. Шлюз системных вызовов для INT 0x80 устанавливается так:
sys/i386/i386/machdep.c: setidt(0x80, &IDTVEC(int0x80_syscall), SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));
Поэтому, когда пользовательское приложение выполняет инструкцию INT 0x80, управление переходит к функции _Xint0x80_syscall
, которая располагается в сегменте кода ядра и
будет выполнена c привилегиями диспетчера (supervisor):
Далее инициализируются консоль и DDB:
sys/i386/i386/machdep.c: cninit(); /* skipped */ #ifdef DDB kdb_init(); if (boothowto & RB_KDB) Debugger("Boot flags requested debugger"); #endif
Сегмент состояния задачи (Task State Segment, TSS) - ещё одна структура защищённого режима x86. TSS используется аппаратурой для загрузки сведений о задаче, когда происходит переключение задачи.
Локальная таблица дескрипторов (Local Descriptors Table, LDT) используется для обращения к коду и данным в пользовательском пространстве. Определенно несколько селекторов для указания на LDT. Это шлюзы системных вызовов и селекторы пользовательского кода и данных:
/usr/include/machine/segments.h #define LSYS5CALLS_SEL 0 /* forced by intel BCS */ #define LSYS5SIGR_SEL 1 #define L43BSDCALLS_SEL 2 /* notyet */ #define LUCODE_SEL 3 #define LSOL26CALLS_SEL 4 /* Solaris >= 2.6 system call gate */ #define LUDATA_SEL 5 /* separate stack, es,fs,gs sels ? */ /* #define LPOSIXCALLS_SEL 5*/ /* notyet */ #define LBSDICALLS_SEL 16 /* BSDI system call gate */ #define NLDT (LBSDICALLS_SEL + 1)
Далее происходит инициализация структуры Блока Управления Процессом (Process Control Block, PCB) (struct pcb) из proc0. proc0 это структура типа struct proc, которая описывает процесс ядра. Она всегда существует, пока работает ядро, потому объявлена как глобальная:
sys/kern/kern_init.c: struct proc proc0;
Структура struct pcb - составная часть proc структуры. Она (struct pcb) определена в /usr/include/machine/pcb.h и содержит информацию о процессе, специфичную для архитектуры i386, такую как значения регистров.
mi_startup()
Эта функция выполняет пузырьковую сортировку всех объектов инициализации системы, после чего вызывает эти объекты один за другим:
sys/kern/init_main.c: for (sipp = sysinit; *sipp; sipp++) { /* ... skipped ... */ /* Call function */ (*((*sipp)->func))((*sipp)->udata); /* ... skipped ... */ }
Несмотря на то, что подсистема sysinit описана в Руководстве для разработчиков, я всё же опишу здесь её внутренности.
Каждой объект инициализации системы (объект sysinit) создаётся посредством вызова макроса SYSINIT(). Давайте, для примера, рассмотрим sysinit объект announce. Этот объект выводит сообщение copyright:
sys/kern/init_main.c: static void print_caddr_t(void *data __unused) { printf("%s", (char *)data); } SYSINIT(announce, SI_SUB_COPYRIGHT, SI_ORDER_FIRST, print_caddr_t, copyright)
Идентификатором подсистемы для этого объекта является SI_SUB_COPYRIGHT (0x0800001), который следует сразу за SI_SUB_CONSOLE (0x0800000). Итак, copyright сообщение будет напечатано первым, сразу же после инициализации консоли.
Давайте посмотрим на то, что конкретно делает макрос SYSINIT(). Он расширяется (expands) в макрос C_SYSINIT(). Макрос C_SYSINIT() далее расширяется в определение статической структуры struct sysinit с вызовом другого макроса DATA_SET:
/usr/include/sys/kernel.h: #define C_SYSINIT(uniquifier, subsystem, order, func, ident) \ static struct sysinit uniquifier ## _sys_init = { \ subsystem, \ order, \ func, \ ident \ }; \ DATA_SET(sysinit_set,uniquifier ## _sys_init); #define SYSINIT(uniquifier, subsystem, order, func, ident) \ C_SYSINIT(uniquifier, subsystem, order, \ (sysinit_cfunc_t)(sysinit_nfunc_t)func, (void *)ident)
Макрос DATA_SET() расширяется в MAKE_SET(), и именно этот макрос является местом, где скрыта вся магия sysinit:
/usr/include/linker_set.h #define MAKE_SET(set, sym) \ static void const * const __set_##set##_sym_##sym = &sym; \ __asm(".section .set." #set ",\"aw\""); \ __asm(".long " #sym); \ __asm(".previous") #endif #define TEXT_SET(set, sym) MAKE_SET(set, sym) #define DATA_SET(set, sym) MAKE_SET(set, sym)
В нашем случае появится следующее определение:
static struct sysinit announce_sys_init = { SI_SUB_COPYRIGHT, SI_ORDER_FIRST, (sysinit_cfunc_t)(sysinit_nfunc_t) print_caddr_t, (void *) copyright }; static void const *const __set_sysinit_set_sym_announce_sys_init = &announce_sys_init; __asm(".section .set.sysinit_set" ",\"aw\""); __asm(".long " "announce_sys_init"); __asm(".previous");
Первая инструкция __asm создаст ELF секцию в пределах ядра. Это произойдёт во время компоновки ядра. Эта секция будет иметь имя .set.sysinit_set. Её содержимое - одно 32-битное значение - адрес структуры announce_sys_init, определяемое второй инструкцией __asm. Третья инструкция __asm помечает конец секции. Если директива с таким же именем секции встречалась раньше, то содержимое, т.е. 32-битное значение, будет добавлено к существующей секции, формируя таким образом массив 32-битных указателей.
Запустив objdump с двоичным файлом ядра в качестве параметра, вы можете заметить наличие таких маленьких секций:
% objdump -h /kernel 7 .set.cons_set 00000014 c03164c0 c03164c0 002154c0 2**2 CONTENTS, ALLOC, LOAD, DATA 8 .set.kbddriver_set 00000010 c03164d4 c03164d4 002154d4 2**2 CONTENTS, ALLOC, LOAD, DATA 9 .set.scrndr_set 00000024 c03164e4 c03164e4 002154e4 2**2 CONTENTS, ALLOC, LOAD, DATA 10 .set.scterm_set 0000000c c0316508 c0316508 00215508 2**2 CONTENTS, ALLOC, LOAD, DATA 11 .set.sysctl_set 0000097c c0316514 c0316514 00215514 2**2 CONTENTS, ALLOC, LOAD, DATA 12 .set.sysinit_set 00000664 c0316e90 c0316e90 00215e90 2**2 CONTENTS, ALLOC, LOAD, DATA
Этот снимок экрана демонстрирует, что секция .set.sysinit_set имеет размер 0x664 байт, поэтому 0x664/sizeof(void *) объектов sysinit скомпилировано в ядро. Другие секции, например .set.sysctl_set, представляют другие компоновочные наборы (linker sets).
При определении переменной типа struct linker_set содержимое секции .set.sysinit_set будет ''собрано'' в этой переменной:
sys/kern/init_main.c: extern struct linker_set sysinit_set; /* XXX */
Структура struct linker_set определена следующим образом:
/usr/include/linker_set.h: struct linker_set { int ls_length; void *ls_items[1]; /* really ls_length of them, trailing NULL */ };
Значение первого поля будет равно числу sysinit объектов. Вторым полем будет NULL-terminated массив указателей на эти объекты.
Вернёмся к обсуждению функции mi_startup()
. Теперь уже
должно быть ясно, как sysinit объекты организованы. Функция mi_startup()
сортирует их и каждого вызывает. Самый последний
объект - системный планировщик:
/usr/include/sys/kernel.h: enum sysinit_sub_id { SI_SUB_DUMMY = 0x0000000, /* not executed; for linker*/ SI_SUB_DONE = 0x0000001, /* processed*/ SI_SUB_CONSOLE = 0x0800000, /* console*/ SI_SUB_COPYRIGHT = 0x0800001, /* first use of console*/ ... SI_SUB_RUN_SCHEDULER = 0xfffffff /* scheduler: no return*/ };
Sysinit объект системного планировщика определён в файле sys/vm/vm_glue.c, и его точка входа - функция scheduler()
. Эта функция на самом деле является бесконечным
циклом и представляет процесс с PID 0 (процесс swapper). Структура proc0, упомянутая
раньше, используется для его описания.
Первый пользовательский процесс, имеющий имя init создаётся sysinit объектом init:
sys/kern/init_main.c: static void create_init(const void *udata __unused) { int error; int s; s = splhigh(); error = fork1(&proc0, RFFDG | RFPROC, &initproc); if (error) panic("cannot fork init: %d\n", error); initproc->p_flag |= P_INMEM | P_SYSTEM; cpu_set_fork_handler(initproc, start_init, NULL); remrunqueue(initproc); splx(s); } SYSINIT(init,SI_SUB_CREATE_INIT, SI_ORDER_FIRST, create_init, NULL)
Функция create_init()
выделяет новый процесс посредством
вызова fork1()
, но не помечает его, как работоспособного.
Когда этот новый процесс будет запланирован планировщиком на выполнение, будет вызвана
функция start_init()
. Она определена в файле init_main.c. Эта функция пытается загрузить и выполнить двоичный
файл init, пробуя сначала /sbin/init,
затем /sbin/oinit, /sbin/init.bak, и
наконец, /stand/sysinstall:
sys/kern/init_main.c: static char init_path[MAXPATHLEN] = #ifdef INIT_PATH __XSTRING(INIT_PATH); #else "/sbin/init:/sbin/oinit:/sbin/init.bak:/stand/sysinstall"; #endif
Пред. | Начало | След. |
Стадия загрузчика | Уровень выше | Замечания по блокировке |
Этот, и другие документы, могут быть скачаны с ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.
По вопросам, связанным с FreeBSD, прочитайте документацию прежде чем писать в <questions@FreeBSD.org>.
По вопросам, связанным с этой документацией, пишите <doc@FreeBSD.org>.
По вопросам, связанным с русским переводом документации, пишите в рассылку <frdp@FreeBSD.org.ua>.
Информация по подписке на эту рассылку находится на сайте проекта перевода.