Pramode C.E. :
The Linux Kernel 0.01 Commentary
Оригинал статьи можно найти на http://pramode.net .
Для понимания работы ядра 0.01 необходимо разобраться в общих теоретитеских концепциях операционных систем,
понять архитектуру 8086,работу ассемблера,компилятора,линкера.
Можно скачать Intel 80386 Manual с адреса http://x86.ddj.com/intel.doc/386manuals.htm
или посмотреть у меня на сайте в разделе /Languages/Asm/Intel 386 manuals .
Перед компиляцией необходимо отредактировать исходник . Современные компиляторы отличаются от того ,
которым компилировалось ядро 0.01 в 1991 г. Необходимо во всех ассемблерных файлах , а также в си-шных
файлах там , где имеются inline-вставки , перед именами переменных убрать символ подчеркивания .
Символ комментария в файлах с расширением .as | нужно поменять на ; .
Необходимо модифицировать все Makefile . Изменения касаются опций ld , as, gcc .
Команде ld добавлена опция -r -T .
Команде as добавлена опция -b .
Был модифицирован файл build.c .Код в этом файле выполняет следующие вещи :
1. удаляет заголовки из обьектного файла , полученного путем компиляции boot.s
2. удаляет заголовка из файла 'system' и генерирует и добавляет в него загрузочный сектор .
build.c должен просто произвести загрузочный код размером 512 байт , который должен быть слеплен
с 'system' командой 'cat' , и в результате должен получиться загрузочный файл 'Image'.
Ядро 0.01 в качестве корневой файловой системы использует файловую систему minix .
Поэтому винт должен быть на primary .C помощью команды fdisk нужно установить его тип как 80(Old Minix).
Для форматирования нужно использовать команду
mkfs.minix /dev/hda4 -n 14
14 - максимальная длина имени файла .
Далее нужно создать 2 каталога :
bin
dev
Нужно создать ноды , соответствующие корневой партиции (hda4) и консоли (tty0).Номера устройств можно найти
в файле /include/linux/fs.h . Можно положить sh в каталог bin .
Необходимо откорректировать файл config.h . С помощью fdisk нужно найти число цилиндров ,секторов и заголовков
на винте.Число заголовков не должно превышать 64 . Константу heads в этом файле нужно изменить с 16 на 64 .
Оригинальный код в файле hd.c не работает с винтами обьемом более 10 гиг , но если закомментировать 1 строку,
то все начинает работать . Этот код также позволяет работать и с secondary .
Файл config.h - изменена константа ROOT_DEV на ту , которая у вас , например , для hda4 ROOT_DEV=304,
для hda3 ROOT_DEV=303. В структуре LINUS_HD нужно поменять WPCOM,LANDZ на CTL 8 .
В файле /include/string.h многие переменные имеют префикс __res __asm__ , который был заменен на просто на __res .
В файле /include/asm/segment.h возвращаемое значение из функции имеет неопределенный спецификатор регистра (=r),
которые все были заменены на (=a).
В файле /kernel/hd.c константе NR_HD было присвоено значение 2 .В функции hd_out() была закомментирована
одна строка , из-за которой не работали винты более 10 гигабайт .
В этой же функции строка head>15 была заменена на head>63 .
Для того , чтобы работать и компилировать программы под запущенным ядром 0.01 , нужно помнить ,
что формат исполняемых файлов в 0.01 - a.out .Технология следующая : после компиляции используем ld
для генерации raw binary output . Затем нужно специально написанной утилитой нужно прочитать размер полученого
бинарника и приаттачить к нему заголовок . Этот код можно найти в linux-0.01/bin/shell/header/header.c.
Далее автор статьи пишет , что они тестили на 2-х машинах . Сначала они запустили на AMDK6 , и все заработало .
Затем на Pentium 1 возник protection fault . Дамп показал , что затык произошел в /boot/head.s .
Пришлось очищать флаг NT.
Сегментация в 386 .
Если взять к примеру архитектуру 8086 , то там каждый сегмент памяти имеет базовый адрес, который получается
просто умножением содержимого сегментного регистра на 16 , после чего к результату добавляется смещение .
В архитектуре 386 содержимое сегментного регистра не имеет прямого отношения к базовым адресам .
Здесь существует специальная map-таблица , которая преобразует содержимое сегментного регистра в базовый адрес.
В этой таблице будут храниться базовые адреса сегментов , при этом
MAP_TABLE[0]=0x0,MAP_TABLE[1]=0x8,MAP_TABLE[2]=0x10,MAP_TABLE[3]=0x18,и т.д.
Сегментный регистр 16-разряден , и интерпретация его различна как в 8086 , так и в 386 .
В 386 :
биты 0,1,2 имеют специальное значение .
биты с 4 по 15 интерпретируются как индекс дескрипторной таблицы для получения базового адреса .
Дескрипторных таблиц может быть 2 :
LDT
GDT
В зависимости от того , в каком состоянии (0 или 1)находится 2-й бит сегментного регистра , мы будем обращаться
или к LDT , или к GDT . Для инициализации этих таблиц существует команды LGDT , LLDT.
В 386 имеются 2 специальных регистра для хранения базового адреса этого таблицы и её размера .
Одна ячейка дескрипторной таблицы хранит 8 байт и называется дескриптором . Селектор отличается от дескриптора
тем,что равен 16 байтам и хранится в сегментном регистре .
Механизм адресации в 386 состоит из 2-х блоков - сегментация плюс пэйджинг . Пэйджинг - необязательный блок,
т.е. он может быть задисэблен . Если же он enable , то адрес , полученный в блоке сегментации , походит обработку
в блоке пэйджинга .
Адресная шина в 386 - 32-битная.Поэтому максимальный размер памяти,который может быть приаттачен к такой шине,
равен 2^32 , т.е. 4 гига . Вся доступная память разбита на 4-кб страницы .
Рассмотрим пример , когда у нас всего 16 метров памяти . Стартовые адреса первых наших страниц будут
0х0,0х1000,0х2000,0х3000,...,0хfff000 .Т.е. мы имеем 4кб страниц по 4 килобайта каждая.
Предположим теперь , что виртуальные адреса , с которыми имеет дело наша программа , лежат в диапазоне
0xf0000000 до 0х000fefff . Если разбить эту область на 4 килобайта , получим 254 страницы , или 1016 кб .
Теперь предположим , что программа загружается по физическому адресу 0х5000 . Это будет означать , что
виртуальный адрес 0xf0000000 будет соответствовать физическому 0х5000 . Map table будет содержать
физические адреса страниц нашей программы . Когда пользовательская программа будет генерировать адреса ,
они будут поставлены в соответствие адресам из мап-таблицы. Эти адреса имеют 32-битную длину.
Пэйджинг в 386 2-уровневый. Таблица , используемая на 1-м уровне индексации , называется page directory .
Ее адрес хранится в регистре cr3 и размер ее 4 кб . В ней находятся адреса 1024 page tables для 2-го уровня
индексации . В ядре 0.01 все процессы имеют одну и ту же page directory .
Прерывания : внешние устройства подсоединены к контроллеру - 8259 PIC или APIC . Когда внешнее устройство
генерирует прерывание , PIC посылает сигнал . Затем один байт посылается на шину данных .
Существует таблица прерываний - IDT - Interrupt Service Rutnes . Эта таблица содержит адреса процедур ,
вызываемых прерываниями .
При попытке записать данные в сегмент , который только на чтение , 386 сгенерирует FAULT . Каждый тип
такого fault имеет идентификатор . Первые 32 строки в таблице IDT зарезервированы под исключения .
Unix - многозадачная система . Существует специальная task table , в которой сохраняется информация о всех
выполняющихся процессах , в частности содержимое всех регистров , всех доступных портов . Сохранением этой
информации должен быть озабочен программист , потому что процессор этим не занимается .
Task Table называется Task State Structure (TSS) , и каждая задача должна иметь свою TSS .
При создании нового процесса создается новая TSS . В текущей TSS всегда хранятся координаты
задачи , на которую нужно переключиться . Переключение между задачами может происходить в результате
вызова инструкций CALL,JMP,INT.
Ядро 0.01 использует только 2 уровня привилегий процессора из четырех - 0-й и 3-й . 0-й используется для кода
ядра и 3-й для кода пользовательских программ .
В TSS имеется 4 пары стековых псевдо-регистров - ss0:esp0,ss1:esp1,ss2:esp2,ss3:esp3. И они соответствуют
4 уровням привилегий . Причем одна и та же задача будет иметь различное содержание в этих псевдо-регистрах
для разных уровней привилегий .
Source Tree .
Код ядра состоит из 3-х главных компонентов :
1. файловая система
2. ядро - шедулер,system calls,signals и т.д.
3. память
Они представлены каталогами fs , kernel , mm .
В каталоге lib лежат системные вызовы , вызываемые из пользовательских программ. Они не нужны для загрузки ,
но необходимы например для работы шелла после загрузки .
Подкаталоги fs , kernel , mm имеют свои собственные makefile , которые производят соответственно обьектные
файлы fs.o , kernel.o , mm.o . Они складываются с main.o , head.o , и в результате получается файл system
в подкаталоге /tools , формат у этого файла raw binary . Далее из файла boot.s получаем raw binary файл
boot . Затем к этому файлу прибавляется необходимое количество байт до размера 512 - эту задачу выполняет
файл build.c. Полученный файл "лепится" с откомпилированным до этого обьектным ядром и в корне исходников
получаем файл Image . Этот файл копируется на дискету , используя команду dd.
Boot Sector
BIOS при загрузке интересуют лишь первые 512 байт дискеты . При этом 511-й байт должен иметь значение 0х55,
512-й байт - 0хaa . После чего эти 512 байт копируются с дискеты в память по адресу 0х7с00 , с которого
и происходит загрузка . Первое , что делает запускаемый код - он клонирует самоё себя по адресу 0х90000.
Автор статьи далее описывает , что он попытался запустить код с адреса 0х7с00 , но код не заработал :-)
После этого загруженный boot-сектор начинает читать оставшийся код ядра с дискеты и грузить его в память
по адресу 0х10000 . После этого ядро копируется с этого адреса на адрес 0х0 . После чего управление
переходит на адрес 0х0 , загрузчик больше не нужен и почивает в бозе .
Кстати сказать , в современных ядрах boot-сектор превышает 512 байт и разбивается на 2 файла - boot.s и
setup.s .
Kernel initialization
Далее начинает работать код , который лежит в /boot/head.s . С этого момента ядро работает
в защищенном режиме. Активируется стек ядра , пэйджинг , таблицы IDT и GDT . Далее контроль передается
в /init/main.c . Таблица IDT дополняется прерываниями .
Ядро готово к обработке пользовательских программ . Функция init генерирует несколько других процессов,
таких , как file system checker , шелл . После этого на экране появляется командное
приглашение , и мы можем запускать свои собственные команды .
0.01 стал полностью "операбельным" ядром.
Сердцевиной любой операционки является работа таймера . Прерывание таймера - единственное , которое генерится
всегда и постоянно , в отличие , скажем , от прерываний клавиатуры или харда .
Операционка постоянно должна следить за переключением процессов .
Таймер увеличивает инкремент у процесса , тем самым мы знаем , как долго процесс выполняется .
По этому счетчику делается вывод о том , надо ли переключаться на иной процесс .
При переключении берется указателья из TR - task-регистра - который указывает на TSS
процесса , на который надобно переключиться . Когда процессов пользователя нет совсем,выполняется т.н.
idle task , который специально генерится ядром на случай простоя.
Когда при запуске очередного процесса может не хватить физической памяти , ядро займется своппингом -
выделением памяти на харде , т.е. начнет сохранять выполняемые инструкции не в памяти , а на диске .
Рассмотрим более подробно , что происходит при загрузке ядра . Мы компилируем ядро 0.01 и получаем Image ,
который готов запуститься с адреса 0x0 . При включении PC загружается boot-сектор БИОС-а,
который проверяет железо . Он читает дискету , на которой находится откомпилированный образ ядра 0.01 .
Если 511-й и 512-й байтики на дискетке те самые заветные , биос грузит эти 510 байт в память ,
после чего загруженные байтики в памяти грузятся уже сами . Этот бут-лоадер - см. файл /boot/boot.s -
начинает грузить ядро с дискеты в память по адресу 0х0 . После чего загруженный boot-сектор
передает управление ядру , при этом происходит переключение с режима 8086 на защищенный режим .
Ядро стартует с кода , который лежит в /boot/head.s . Первое , что нужно сделать - это проинициализировать
таблицы IDT , GDT , LDT , проинициализировать page tables . После этих приготовлений мы переходим в
/init/main.c . Здесь запускаются несколько основных процессов - init-процесс , процесс сканирования файловой системы ,
шелл .
- файл /kernel/system_call.s . Он обрабатывает прерывание 0х80 , которое используется
для вызова system call . Кроме этого , файл включает в себя код управления signal , fork , exec ,
прерываниями таймера и прерываниями харда .
- файл /kernel/sys.c - включает код для обработки других system calls .
- файлы /kernel/asm.s , /kernel/traps.c - включает код для обработки исключений .
- файлы /kernel/console.c , /kernel/tty_io.c - включает код для обработки консоли .
- файлы /kernel/panic.c , /kernel/mktime.c , /kernel/vsprintf.c ,/kernel/printk.c-
включает определение таких функций , как printf , printk .
- файлы /kernel/hd.c , /kernel/keyboard.s , /kernel/rs_io.s - управление устройствами
- файлы /kernel/fork.c , /kernel/exit.c -
- файлы /kernel/shed.c - управление таймером , сердцевина ОС . Шедулер занимается переключением процессов .
Каталог /mm включает код для управления памятью .
- файл /mm/page.s - происходит обработка page fault , которая может произойти по 2 причинам -
либо попытка записать в память , которая только на чтение , либо попытка открыть несуществующую
страницу памяти - при обнаружении этих 2-х ошибок управление передается в memory.c .
Файловая система
- файл /fs/super.c - читает супер-блок , распознает файловую систему и инициализирует root inode .
- файл /fs/bufer.c - служит для оптимизации чтения-записи
- файл /fs/bitmap.c,/fs/inode.c , /fs/namei.c , /fs/truncate.c - ядро вначале монтирует корневую
файловую систему и присваивает ей root inode . Доступ к файловой системе будет осуществляться
через inode .
- файл /fs/block_dev.c , /fs/file_dev.c , /fs/char_dev.c - в 0.01 поддерживаются 3 типа устройств -
блочные,файловые и символьные .
- файл /fs/file_table.c - просто массив файлов , открытых в данный момент
- файл /fs/open.c , /fs/read_write.c
- файл /fs/fcntl , /fs/ioctl.c , /fs/tty_ioctl.c , /fs/stat.c - операции , связанные с файловыми манипуляциями
- файл /fs/pipe.c
- файл /fs/exec.c - выполняет загрузку исполняемого файла с диска и его последующий запуск
- каталог /linux/lib/ - используются для работы с пользовательскими программами
- каталог /linux/tools/ - включает 1 файл build.c - это конструктор , который лепит заключительный
файл Image .
Файл /boot/boot.s
Первоначально биос грузит загрузчик c дискеты по адресу 0x7c00 ,
после чего загрузчик сам клонирует себя по адресу 0x90000 .
Процессор при этом находится в real mode . Почему загрузчик клонирует себя
по адресу 0х90000 ? Потому что в более нижние адреса этого делать нежелательно
из-за возможных конфликтов с биосом . Ядро будет загружено по адресу 0х10000 .
Как известно БИОС оперирует в мервом мегабайте памяти .
Итак , после того как мы загрузили ядро , биос нам более не нужен .
Переключение в защищенный режим происходит в boot.s .
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
BOOTSEG = 0x07c0
INITSEG = 0x9000
SYSSEG = 0x1000 | system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE
entry start
start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
В приведенном тексте происходит копирование 512 байт с адреса BOOTSEG в адрес INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,#0x400 | arbitrary value >>512
mov ah,#0x03 | read cursor pos
xor bh,bh
int 0x10
mov cx,#24
mov bx,#0x0007 | page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 | write string, move cursor
int 0x10
| ok, we've written the message, now
| we want to load the system (at 0x10000)
А сейчас мы только-что распечатали сообщение на экране
Следующий код читает ядро с дискеты и копирует его в память по адресу 0х10000
mov ax,#SYSSEG
mov es,ax | segment of 0x010000
call read_it
call kill_motor
Переключение в protected mode :
cli
Теперь копируем ядро из памяти с адреса 0х10000 в адрес 0х0000
| first we move the system to it's rightful place
mov ax,#0x0000
cld | 'direction'=0, movs moves forward
do_move:
mov es,ax | destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax | source segment
sub di,di
sub si,si
mov cx,#0x8000
rep
movsw
j do_move
Дальнейшая подготовка protected mode включает инициализацию таблиц GDT и IDT
Внутри кода boot.s можно найти 2 метки :
idt_48:
gdt_48:
которые и представляют из себя адреса этих таблиц в памяти .
Следующий код с помощью контроллера клавиатуры получает доступ к 4 Gb памяти
(для данных)
| that was painless, now we enable A20
call empty_8042
mov al,#0xD1 | command write
out #0x64,al
call empty_8042
mov al,#0xDF | A20 on
out #0x60,al
call empty_8042
Следующий код инициализирует контролер прерываний 8259 , у которого
имеются свои регистры для чтения . Нужно помнить , что первые 32 прерывания
Интел зарезервировал для своих нужд , поэтому мы настраиваем контролер
на стартовое прерывание по адресу 0х20 .
mov al,#0x11 | initialization sequence
out #0x20,al | send it to 8259A-1
.word 0x00eb,0x00eb | jmp $+2, jmp $+2
out #0xA0,al | and to 8259A-2
.word 0x00eb,0x00eb
mov al,#0x20 | start of hardware int's (0x20)
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x28 | start of hardware int's 2 (0x28)
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x04 | 8259-1 is master
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x02 | 8259-2 is slave
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x01 | 8086 mode for both
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0xFF | mask off all interrupts for now
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
А теперь само переключение в protected mode , и загрузчик прыгает
в начальный адрес памяти :
mov ax,#0x0001 | protected mode (PE) bit
lmsw ax | This is it!
jmpi 0,8 | jmp offset 0 of segment 8 (cs)
.word 0x00eb,0x00eb
in al,#0x64 | 8042 status port
test al,#2 | is input buffer full?
jnz empty_8042 | yes - loop
ret
Что происходит дальше ?
Мы копируем ядро с адреса 0х000 , используя индексную адресацию es:[bx].
Вначале мы инициализируем :
es = 0x0
bx = 0x0
Дальше идет цикл инкремента bx до значения bx = 04ffff .
Затем мы прибавляем :
es + 0x1000
и снова делаем
bx = 0x0
Адресация в x86 вычисляется по формуле :
es * 4 + bx
Процедура read_track будет использовать БИОС для загрузки необходимого числа секторов :
Вычисление размера сегмента ENDSEG
sread: .word 1 | sectors read of current track
head: .word 0 | current head
track: .word 0 | current track
read_it:
mov ax,es
test ax,#0x0fff
die: jne die | es must be at 64kB boundary
xor bx,bx | bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG | have we loaded all yet?
jb ok1_read
ret
Умножаем cx на 512 - размер сектора :
ok1_read:
mov ax,#sectors
sub ax,sread
mov cx,ax
shl cx,#9
Вычислим , сколько нам не хватает байт для текущего сегмента :
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
Таблица gdt создается в 2-х экземплярах - одна для кода и другая
для данных каждая размером по 8 метров с соответствующими правами.
gdt:
.word 0,0,0,0 | dummy
.word 0x07FF | 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 | base address=0
.word 0x9A00 | code read/exec
.word 0x00C0 | granularity=4096, 386
.word 0x07FF | 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 | base address=0
.word 0x9200 | data read/write
.word 0x00C0 | granularity=4096, 386
Следующий код обнуляет таблицу прерываний - они нам не нужны :
idt_48:
.word 0 | idt limit=0
.word 0,0 | idt base=0L
Все - boot.s отработал свое .
Посмотрим теперь на head.s
Следующий код нужен для инициализации page directory
$0x10 - это адрес data segment :
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
Далее инициализация стека .
stack_start - это базовая структура стека , которая прописана /kernel/shed.c
lss _stack_start,%esp
Setup IDT и GDT :
call setup_idt
call setup_gdt
Инициализируем все сегментные регистры :
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp
Прыгаем по адресу на метке after_page_tables . Вся память ниже этой метки
уйдет на paging :
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000
cmpl %eax,0x100000
je 1b
movl %cr0,%eax # check math chip
andl $0x80000011,%eax # Save PG,ET,PE
testl $0x10,%eax
jne 1f # ET is set - 387 is present
orl $4,%eax # else set emulate bit
1: movl %eax,%cr0
jmp after_page_tables
Инициализируем таблицу IDT , состоящую из 256 адресов :
setup_idt:
lea ignore_int,%edx
movl $0x00080000,%eax
movw %dx,%ax /* selector = 0x0008 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea _idt,%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
lidt idt_descr
ret
К следующей порции кода небольшой комментарий .
Интел использует 2-уровневый paging -
1 уровень - это одна страница 1-го уровня ,называемая page directory
2 уровень - 1024 страницы , называемые page tables
Page directory начинается с адреса 0х0 и заканчивается 0х100 (4К)
Далее мы используем 2 page tables - первая по адресу 0х10000 (pg0)
и 0х20000 (pg1). Итого памяти у этих 2 таблиц :
2 * 1024 = 8 MB
Есть еще одна страница - pg2 (0x30000-0x40000),но она не используется.
Вся эта память ТОЛЬКО для ядра . Каждый пользовательский процесс
будет создавать свои собственные page directory/page tables :
setup_gdt:
lgdt gdt_descr
ret
.org 0x1000
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2: # This is not used yet, but if you
# want to expand past 8 Mb, you'll have
# to use it.
.org 0x4000
После этого мы прыгаем в C-main-функцию :
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $_main
jmp setup_paging
L6:
jmp L6 # main should never return here, but
Обнуляем pg0 , pg1 :
/* This is the default interrupt "handler" :-) */
.align 2
ignore_int:
incb 0xb8000+160 # put something on the screen
movb $2,0xb8000+161 # so that we know something
iret # happened
/*
* Setup_paging
*
* This routine sets up paging by setting the page bit
* in cr0. The page tables are set up, identity-mapping
* the first 8MB. The pager assumes that no illegal
* addresses are produced (ie >4Mb on a 4Mb machine).
*
* NOTE! Although all physical memory should be identity
* mapped by this routine, only the kernel page functions
* use the >1Mb addresses directly. All "normal" functions
* use just the lower 1Mb, or the local data space, which
* will be mapped to some other place - mm keeps track of
* that.
*
* For those with more memory than 8 Mb - tough luck. I've
* not got it, why should you :-) The source is here. Change
* it. (Seriously - it shouldn't be too difficult. Mostly
* change some constants etc. I left it at 8Mb, as my machine
* even cannot be extended past that (ok, but it was cheap :-)
* I've tried to show which constants to change by having
* some kind of marker at them (search for "8Mb"), but I
* won't guarantee that's all :-( )
*/
.align 2
setup_paging:
movl $1024*3,%ecx
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl
Заполним первые 2 строки в page directory указателями на pg0 и pg1 :
movl $pg0+7,_pg_dir /* set present bit/user r/w */
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
Теперь заполним page tables физическими адресами . Например
в последней строке pg1 будет адрес , соответствующий 8 М памяти.
В адрес входят биты для прав :
movl $pg1+4092,%edi
movl $0x7ff007,%eax /* 8Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
Установим page directory на адрес 0х0 :
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
.align 2
.word 0
idt_descr:
.word 256*8-1 # idt contains 256 entries
.long _idt
.align 2
.word 0
gdt_descr:
.word 256*8-1 # so does gdt (not that that's any
.long _gdt # magic number, but it works for me :^)
.align 3
_idt: .fill 256,8,0 # idt is uninitialized
Теперь переходим в каталог /kernel
Рассмотрим файл /kernel/system_call.s
/*
* system_call.s contains the system-call low-level handling routines.
* This also contains the timer-interrupt handler, as some of the code is
* the same. The hd-interrupt is also here.
*
* NOTE: This code handles signal-recognition, which happens every time
* after a timer-interrupt and after each system call. Ordinary interrupts
* don't handle signal-recognition, as that would clutter them up totally
* unnecessarily.
*
* Stack layout in 'ret_from_system_call':
*
* 0(%esp) - %eax
* 4(%esp) - %ebx
* 8(%esp) - %ecx
* C(%esp) - %edx
* 10(%esp) - %fs
* 14(%esp) - %es
* 18(%esp) - %ds
* 1C(%esp) - %eip
* 20(%esp) - %cs
* 24(%esp) - %eflags
* 28(%esp) - %oldesp
* 2C(%esp) - %oldss
*/
Код этого файла выполняется в стеке . Причем у ядра и пользовательских
процессов (в случае применения fork) будут разные стеки.
При переключении уровней с 0 на 3 регистры ess,esp,eflags,cs
также хранятся в кернел-стеке .
В метку _system_call мы приходим каждый раз , когда пользовательский
процесс вызывает системный вызов - int 0x86 .
SIG_CHLD = 17
EAX = 0x00
EBX = 0x04
ECX = 0x08
EDX = 0x0C
FS = 0x10
ES = 0x14
DS = 0x18
EIP = 0x1C
CS = 0x20
EFLAGS = 0x24
OLDESP = 0x28
OLDSS = 0x2C
state = 0 # these are offsets into the task-struct.
counter = 4
priority = 8
signal = 12
restorer = 16 # address of info-restorer
sig_fn = 20 # table of 32 signal addresses
nr_system_calls = 67
.globl _system_call,_sys_fork,_timer_interrupt,_hd_interrupt,_sys_execve
.align 2
bad_sys_call:
movl $-1,%eax
iret
.align 2
reschedule:
pushl $ret_from_sys_call
jmp _schedule
.align 2
_system_call:
При этом номер вызова передается в регистре eax :
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
Регистры ds,es указывают на пространство ядра ,
регистр fs - на пространство пользователя :
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
Далее настройка виртуального адреса системного вызова.
Функция sys_call_table прописана в /include/linux/sys.h :
mov %dx,%fs
call _sys_call_table(,%eax,4)
Информация о пользовательском процессе хранится в специальной структуре
Адрес этой структуры хранится в переменной current .
В зависимости от содержимого этой структуры ядро принимает решение ,
надо ли переключаться на другой процесс :
pushl %eax
movl _current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
После того , как системный вызов выполнен , процессу надо
послать сигнал . Ядро при этом устанавливает нужный бит в специальном
массиве сигналов
ret_from_sys_call:
movl _current,%eax # task[0] cannot have signals
cmpl _task,%eax
je 3f
movl CS(%esp),%ebx # was old code segment supervisor
testl $3,%ebx # mode? If so - don't check signals
je 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
Как процесс возобновляет свою работу именно с того места ,
откуда он сделал вызов system_call ? Просто зачение регистра
EIP восстанавливается из стека
xchgl %ebx,EIP(%esp) # put new return address on stack
subl $28,OLDESP(%esp)
movl OLDESP(%esp),%edx # push old return address on stack
Следующий кусок кода проверяет т.н. page fault - т.е. идет проверка
наличия вызываемой страницы памяти :
pushl %eax # but first check that it's ok.
pushl %ecx
pushl $28
pushl %edx
call _verify_area
Следующий кусок прибивает процесс :
default_signal:
incl %ecx
cmpl $SIG_CHLD,%ecx
je 2b
pushl %ecx
call _do_exit # remember to set bit 7 when dumping core
addl $4,%esp
jmp 3b
Перейдем к файлу /kernel/fork.c
Функция verify_area проверяет , существует ли реальная физическая страница,
соответствующая виртуальному в диапазоне от addr до addr+size .
В каждом процессе есть линейный блок виртуальных адресов.
Стартовый адрес процесса - nr . Возможное виртуальное адрессное
пространство в 0.01 для одного процесса - 64 метра . Поэтому базовое
пространство nr = 0 * 0x4000000 . Все виртуальные адреса генерятся
на этапе линковки пользовательской программы с указанием на
базовый адрес 0х0 . Функция write_verify создает реальную физическую
страницу для соответственного виртуального адреса :
/*
* 'fork.c' contains the help-routines for the 'fork' system call
* (see also system_call.s), and some misc functions ('verify_area').
* Fork is rather simple, once you get the hang of it, but the memory
* management can be a bitch. See 'mm/mm.c': 'copy_page_tables()'
*/
#include
#include < linux/sched.h>
#include < linux/kernel.h>
#include < asm/segment.h>
#include < asm/system.h>
extern void write_verify(unsigned long address);
long last_pid=0;
void verify_area(void * addr,int size)
{
unsigned long start;
start = (unsigned long) addr;
size += start & 0xfff;
start &= 0xfffff000;
start += get_base(current->ldt[2]);
while (size>0) {
size -= 4096;
write_verify(start);
start += 4096;
}
}
Следующая функция - int copy_mem , она используется в другой
функции - copy_process . При вызове fork , потомок должен получить
информацию о родителе . Юникс рассматривает адресное пространство
одного процесса как единый блок виртуальных адресов .
При вызове этой функции имеется ввиду , что в кодовом сегменте и сегменте
данных должны быть одни и те же адреса . При создании нового процесса
эти адреса генерятся как nr + 0x4000000 . Проверяется и добавляется
строка в page directory :
int copy_mem(int nr,struct task_struct * p)
{
unsigned long old_data_base,new_data_base,data_limit;
unsigned long old_code_base,new_code_base,code_limit;
code_limit=get_limit(0x0f);
data_limit=get_limit(0x17);
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
if (data_limit < code_limit)
panic("Bad data_limit");
new_data_base = new_code_base = nr * 0x4000000;
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}
Следующая функция - copy_process , она вызывается из функции _sys_fork,
которая лежит в system_call.s . В свою очередь , _sys_fork , являясь
элементом массива sys_call_table[] , вызывается из другой функции -
system_call .
В функции int copy_process выделяется страница памяти для task structure
процесса . Затем копируем поля из родительской структуры в дочернюю -
*p = *current;
В конце 2 адреса - TSS и LDT - сохраняются в gdt :
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_RUNNING;
p->pid = last_pid;
p->father = current->pid;
p->counter = p->priority;
p->signal = 0;
p->alarm = 0;
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0;
p->cutime = p->cstime = 0;
p->start_time = jiffies;
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10;
p->tss.eip = eip;
p->tss.eflags = eflags;
p->tss.eax = 0;
p->tss.ecx = ecx;
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff;
p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT(nr);
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("fnsave %0"::"m" (p->tss.i387));
if (copy_mem(nr,p)) {
free_page((long) p);
return -EAGAIN;
}
for (i=0; ifilp[i])
f->f_count++;
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
task[nr] = p; /* do this last, just in case */
return last_pid;
}
Перейдем к /kernel/sched.c
Функция schedule проверяет , насколько реальна причина для того ,
чтобы пробудить тот или иной процесс . Когда процесс засыпает ,
он может быть , а может и не быть пробужден .
p->counter указывает на число тактов , в течение которых процесс
может быть в работе перед тем , как заснуть . Когда этот счетчик
становится равным нулю , процесс переходит в спячку .
Существуют конечно и другие причины помимо этого каунтера ,
по которым процесс может перейти в спячку . Функция ниже из всех
процессов переводит в спячку тот , у которых каунтер минимален .
Переключение на другой процесс происходит с помощью асм-функции
switch_to :
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
/* check alarm, wake up any interruptible tasks that have got a signal */
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) {
if ((*p)->alarm && (*p)->alarm < jiffies) {
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0;
}
if ((*p)->signal && (*p)->state==TASK_INTERRUPTIBLE)
(*p)->state=TASK_RUNNING;
}
/* this is the scheduler proper: */
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(next);
}
Следующий системный вызов маркирует процесс как прерываемый :
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE;
schedule();
return 0;
}
Процесс переводится в спячку и выстраивается в очередь ,
представляющую linked list .
Существует специальный буфер , в котором находится текущий процесс
из очереди
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp = *p;
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
schedule();
if (tmp)
tmp->state=0;
}
Перейдем к файлу /kernel/hd.c - он описывает работу с хардом.
Все сделано через linked list .
В зависимости от того , каков запрос - на чтение или запись -
соответственно указатель-функция do_hd будет установлен на функцию ,
которая управляет прерыванием диска контроллера харда на чтение
или запись соответственно . Если запрос на чтение , то контроллер
сгенерит прерывание тогда , когда данные будут готовы к копированию
в буфер . Далее вызывается функция do_hd . При записи все та же do_hd
запишет данные из буфера на диск . Запросы выстраиваются в очередь :
static struct hd_i_struct{
int head,sect,cyl,wpcom,lzone,ctl;
} hd_info[]= { HD_TYPE };
#define NR_HD ((sizeof (hd_info))/(sizeof (struct hd_i_struct)))
static struct hd_struct {
long start_sect;
long nr_sects;
} hd[5*MAX_HD]={{0,0},};
static struct hd_request {
int hd; /* -1 if no request */
int nsector;
int sector;
int head;
int cyl;
int cmd;
int errors;
struct buffer_head * bh;
struct hd_request * next;
} request[NR_REQUEST];
#define IN_ORDER(s1,s2) \
((s1)->hd<(s2)->hd || (s1)->hd==(s2)->hd && \
((s1)->cyl<(s2)->cyl || (s1)->cyl==(s2)->cyl && \
((s1)->head<(s2)->head || (s1)->head==(s2)->head && \
((s1)->sector<(s2)->sector))))
static struct hd_request * this_request = NULL;
static int sorting=0;
static void do_request(void);
static void reset_controller(void);
static void rw_abs_hd(int rw,unsigned int nr,unsigned int sec,unsigned int head,
unsigned int cyl,struct buffer_head * bh);
void hd_init(void);
...
void (*do_hd)(void) = NULL;
Перейдем к /mm/memory.c
В основном этот код занимается распределением свободных виртуальных ресурсов
в памяти . Напомню , что все что ниже 0х100000 - используется ядром.
Обьем массива физических страниц зашит в самой системе .
Свободная физическая страница находится с помощью массива mem_map .
После того как свободная страница найдена , ее индекс помещается в ecx ,
поэтому физический адрес вычисляется как (4k * ecx) + 0x100000.
Заполняем эту страницу нолями :
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasw\n\t"
"jne 1f\n\t"
"movw $1,2(%%edi)\n\t"
"sall $12,%%ecx\n\t"
"movl %%ecx,%%edx\n\t"
"addl %2,%%edx\n\t"
"movl $1024,%%ecx\n\t"
"leal 4092(%%edx),%%edi\n\t"
"rep ; stosl\n\t"
"movl %%edx,%%eax\n"
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
:"di","cx","dx");
return __res;
}
Как я уже говорил , в 386 имеется иерархия :
page directory
page table
actual page
Для того чтобы освободить одну actual page , нам нужно пройти
всю эту цепочку . При освобождении одной позиции в page directory
освобождается 4 метра , а одному процессу в данной версии
доступно 64 метра :
void free_page(unsigned long addr)
{
if (addrHIGH_MEMORY)
panic("trying to free nonexistent page");
addr -= LOW_MEM;
addr >>= 12;
if (mem_map[addr]--) return;
mem_map[addr]=0;
panic("trying to free free page");
}
Для того чтобы вычислить нужную page directory , мы помним ,
что в памяти они располагаются в самом низу - начиная с адреса
0х0 :
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;
if (from & 0x3fffff)
panic("free_page_tables called with wrong alignment");
if (!from)
panic("Trying to free up swapper memory space");
size = (size + 0x3fffff) >> 22;
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
for ( ; size-->0 ; dir++) {
if (!(1 & *dir))
continue;
pg_table = (unsigned long *) (0xfffff000 & *dir);
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table)
free_page(0xfffff000 & *pg_table);
*pg_table = 0;
pg_table++;
}
free_page(0xfffff000 & *dir);
*dir = 0;
}
invalidate();
return 0;
}
Следующая функция использует лишь один системный вызов - fork().
Копирование происходит блоками по 4 метра .
Происходит заполнение page tables адресами .
Физические страницы при этом маркируются read-only :
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
unsigned long * from_page_table;
unsigned long * to_page_table;
unsigned long this_page;
unsigned long * from_dir, * to_dir;
unsigned long nr;
if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22;
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
if (!(to_page_table = (unsigned long *) get_free_page()))
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;
nr = (from==0)?0xA0:1024;
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;
*to_page_table = this_page;
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();
return 0;
}
При копировании страницы памяти оба - и источник и получатель -
маркируются на read only . Следующая функция позволяет использовать дочернему
процессу страницу , которая помечена на read only , для записи :
void un_wp_page(unsigned long * table_entry)
{
unsigned long old_page,new_page;
old_page = 0xfffff000 & *table_entry;
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
*table_entry |= 2;
return;
}
if (!(new_page=get_free_page()))
do_exit(SIGSEGV);
if (old_page >= LOW_MEM)
mem_map[MAP_NR(old_page)]--;
*table_entry = new_page | 7;
copy_page(old_page,new_page);
}
Перейдем к /fs/exec.c
Что происходит , когда мы что-то набираем в командной строке ?
Шелл берет эти строки , ложит их в 2-мерный массив и передает в качестве
параметров функции execve .
argv и envp - это адреса в сегменте данных пользователя , поэтому при
обращении к ним ядро будет пользоваться LDT .
Следующая функция делает быстрое копирование из ds:si в es:di .
Для пользователя в es копируется 0x17 , для ядра - 0x10 .
Данные копируются из ядра в пользовательское пространство :
#define cp_block(from,to) \
__asm__("pushl $0x10\n\t" \
"pushl $0x17\n\t" \
"pop %%es\n\t" \
"cld\n\t" \
"rep\n\t" \
"movsl\n\t" \
"pop %%es" \
::"c" (BLOCK_SIZE/4),"S" (from),"D" (to) \
:"cx","di","si")
|