The Real “Hello World”, C / C + +, Програмування, статті

Stanislav Ievlev, linux.ru.net

1. Ідея (hello.c)

Вивчення нової мови програмування починається, як правило, з написання простенької програми, що виводить на екран короткий вітання типу “Hello World!”. Наприклад, для C це буде виглядати приблизно так.

main()
{
printf("Hello World!\n");
}

Показово, але зовсім не цікаво. Програма, звичайно працює, режим захищений, але ж для її функціонування потрібно ЦІЛА операційна система. А що якщо написати такий “Hello World”, для якого нічого не треба. Вставляємо дискетка в комп’ютер, завантажується з неї і … “Hello World”. Можна навіть прокричати це вітання із захищеного режиму.

Сказано – зроблено. З чого б почати? .. Набратися знань, звичайно. Для цього дуже добре полазити в исходники Linux і Thix. Перша система всім добре знайома, друга менш відома, але не менш корисна.

Підучилися? … Зрозуміло, що спершу треба написати завантажувальний сектор для нашої міні-опрераціонкі (адже це саме міні-операційка). Оскільки процесор вантажиться в 16-розрядному режимі, то для созджанія завантажувального сектора використовується асемблер і лінковщік з пакету bin86. Можна, звичайно, пошукати ще щось, але обидва наших прикладу використовують саме його і ми теж піде по стопах вчителів. Синтаксис цього асемблера немколько дивакуватий, що суміщає риси, характерні і для Intel і для AT & T (за подробицями прямуйте в Linux-Assembly-HOWTO), але після пари тижнів мук можна звикнути.

2. Завантажувальний сектор (boot.S)

Свідомо не буду приводити лістингів програм. Так стануть зрозуміліше основні ідеї, та й вам буде набагато приємніше, якщо все напишіть своїми руками.

Для початку визначимося з основними константами.

START_HEAD = 0 – Головка приводу, якою будемо використовувати.

START_TRACK = 0 – Доріжка, звідки почнемо читання.

START_SECTOR = 2 – Сектор, починаючи з якого будемо зчитувати наше ядерце.

SYSSIZE = 10 – Розмір ядра в секторах (кожен сектор містить 512 байт)

FLOPPY_ID = 0 – Ідентифікатор приводу. 0 – для першого, 1 – для другого

HEADS = 2 – Кількість головок приводу.

SECTORS = 18 – Кількість доріжок на дискеті. Для формату 1.44 Mb це кількість дорівнює 18.

В процесі завантаження буде відбуватися наступне. Завантажувач BIOS вважає перший сектор дискети, покладе його за адресою 0000:0 x7c00 і передасть туди управління. Ми його отримаємо і для початку перемістимо себе нижче за адресою 0000:0 x600, перейдемо туди і спокійно продовжимо роботу. Власне вся наша робота буде складатися з завантаження ядра (сектора 2 – 12 першої доріжки дискети) за адресою 0x100: 0000, переходу в захищений режим і стрибка на перші рядки ядра. У зв’язку з цим ще кілька констант:

BOOTSEG = 0x7c00 – Сюди помістить завантажувальний сектор BIOS.

INITSEG = 0x600 – Сюди його перемістимо ми.

SYSSEG = 0x100 – А тут приємно розташується наше ядро.

DATA_ARB = 0x92 – Визначник сегмента даних для дескриптора

CODE_ARB = 0x9A – Визначник сегмента коду для дескриптора.

Насамперед зробимо переміщення самих себе в більш прийнятне місце.

   cli
   xor     ax, ax
   mov     ss, ax
   mov     sp, #BOOTSEG
   mov     si, sp
   mov     ds, ax
   mov     es, ax
   sti
   cld
   mov     di, #INITSEG
   mov     cx, #0x100
   repnz
   movsw jmpi go, # 0; стрибок у нове місце розташування  завантажувального сектора на мітку go

Тепер необхідно налаштувати як слід сегменти для даних (es, ds) і для стека. Це звичайно неприємно, що все доводиться робити вручну, але що робити. Адже немає нікого в пам’яті комп’ютера, крім нас і BIOS.

go:
  mov     ax, #0xF0
  mov     ss, ax mov sp, ax; Стек розмістимо як 0xF0: 0xF0 = 0xFF0 mov ax, # 0x60; Сегменти для даних ES і DS поставимо в 0x60
  mov     ds, ax
  mov     es, ax

Нарешті можна вивести переможний привітання. Нехай світ дізнається, що ми змогли завантажитися. Оскільки у нас є все-таки ще BIOS, скористаємося готової функцією 0x13 переривання 0x10. Можна звичайно знехтувати його і написати прямо в відеопам’ять, але у нас кожен байт команди на рахунку, а байт таких всього 512. Витратимо їх краще на щось більш корисне.

  mov     cx,#18
  mov     bp,#boot_msg
  call    write_message

Функція write_message вигдядіт наступним чином

write_message:
   push    bx
   push    ax
   push    cx
   push    dx
   push    cx mov ah, # 0x03; прочитаємо поточне положення курсору, щоб не виводити повідомлення де попало.
   xor     bh,bh
   int     0x10
   pop     cx mov bx, # 0x0007; Параметри виведених символів: відеосторінок 0, атрибут 7 (сірий на чорному) mov ax, # 0x1301; Виводимо рядок і зрушуємо курсор.
   int     0x10
   pop     dx
   pop     cx
   pop     ax
   pop     bx
   ret

А повідомлення так

boot_msg:
                .byte 13,10
                .ascii "Booting data ..."
                .byte 0

До цього часу на дисплеї комп’ютера з’явиться скромне “Booting data …” . Це в принципі вже “Hello World”, але давайте доб’ємося трішки більшого. Перейдемо в захищений режим і виведемо цей “Hello” вже з програми написання на C.

Ядро 32-розрядне. Воно буде у нас розміщуватися окремо від завантажувального сектора і збиратися вже gcc і gas. Синтаксис асемблера gas відповідає вимогам AT & T, так що тут уже все простіше. Але для Спершу нам потрібно прочитати ядро. Знову скористаємося готової функцією 0x2 переривання 0x13.


recalibrate:
mov ah, #0
mov dl, #FLOPPY_ID int 0x13; виробляємо переініціалізацію дисковода.
jc recalibrate call read_track; виклик функції читання ядра jnc next_work; якщо під час читання не відбулося нічого поганого то працюємо далі
bad_read: ; Якщо читання відбулося невдало то виводимо повідомлення про помилку
mov bp,#error_read_msg
mov cx,7
call write_message inf1: jmp inf1; і йдемо в нескінченний цикл. Тепер нас врятує тільки ручна перезавантаження

Сама функція читання гранично проста: довго і нудно заповнюємо параметри, а потім одним махом зчитуємо ядро. Ускладнення розпочнуться, коли ядро ​​перестане поміщатися в 17 секторах (тобто 8.5 kb), але це поки тільки в майбутньому, а поки цілком достатньо такого блискавичного читання.


read_track:
pusha
push es
push ds mov di, # SYSSEG; Визначаємо mov es, di; адреса буфера для даних
xor bx, bx mov ch, # START_TRACK; доріжка 0 mov cl, # START_SECTOR; починаючи з сектора 2
mov dl, #FLOPPY_ID
mov dh, #START_HEAD
mov ah, #2 mov al, # SYSSIZE; вважати 10 секторів
int 0x13
pop ds
pop es
popa
ret

От і все. Ядро успішно прочитано і можна вивести ще одне радісне повідомлення на екран.

next_work: call kill_motor; зупиняємо привід дисководу mov bp, # load_msg; виводимо повідомлення
  mov     cx,#4
  call    write_message

Ось вміст повідомлення

load_msg:
   .ascii "done"
   .byte 0

А от функція зупинки двигуна приводу.

kill_motor:
  push    dx
  push    ax
  mov     dx,#0x3f2
  xor     al,al
  out     dx,al
  pop     ax
  pop     dx
  ret

На даний момент на екрані виведено “Booting data … done” і лампочка приводу флоппі-дисків погашена. Всі затихли і готові до смертельного номеру – стрибка в захищений режим.

Для початку треба включити адресну лінію A20. Це в точності означає, що ми будемо використовувати 32-розрядну адресацію до даних.

 mov al, # 0xD1; команда записи для 8042
  out     #0x64, al mov al, # 0xDF; включити A20
  out     #0x60, al

Виведемо попередження, про те, що переходимо в захищений режим. Нехай всі знають, які ми важливі.

protected_mode:
   mov     bp,#loadp_msg
   mov     cx,#25
   call    write_message

(Повідомлення:

loadp_msg:
   .byte 13,10
   .ascii "Go to protected mode..."
   .byte 0
 )

Поки що у нас живий BIOS, запам’ятаємо позицію курсору і збережемо її у відомому місці (0000:0 x8000). Ядро пізніше забере всі дані і буде їх використовувати для виведення на екран переможного повідомлення.

save_cursor: mov ah, # 0x03; читаємо поточну позицію курсора
   xor     bh,bh
   int     0x10
   seg     cs mov [0x8000], dx; зберігаємо в спеціальному тайнику

Тепер увагу, забороняємо переривання (нема чого відволікатися під час такої роботи) і завантажуємо таблицю дескрипторів

   cli lgdt GDT_DESCRIPTOR; завантажуємо описувач таблиці  дескрипторів.

У нас таблиця дескрипторів складається з трьох описувачів: Нульовий (завжди повинен бути присутнім), сегменту коду і сегменту даних

.align  4
.word   0 GDT_DESCRIPTOR:. Word 3 * 8 - 1; розмір таблиці  дескрипторів . Long 0x600 + GDT; місце розташування  таблиці дескрипторів
.align  2
GDT: . Long 0, 0; Номер 0: порожній дескриптор . Word 0xFFFF, 0; Номер 8:  дескриптор коду
                .byte   0, CODE_ARB, 0xC0, 0 . Word 0xFFFF, 0; Номер 0x10:  дескриптор даних
                .byte   0, DATA_ARB, 0xCF, 0

Перехід у захищений режим може відбуватися мінімум двома способами, але обидві ОС, обрані нами для прикладу (Linux і Thix) використовують для сумісності з 286 процесором команду lmsw. Ми будемо діяти тим же способом

  mov     ax, #1 lmsw ax; прощай реальний режим. Ми тепер  знаходимося в захищеному режимі. jmpi 0x1000, 8; Затяжний стрибок на 32-розрядне ядро.

Ось і вся робота завантажувального сектора – чимало, але і небагато. Тепер ми попрощаємося з ним і попрямуємо до ядра.

В кінці ассемблерного файлу корисно додати наступну інструкцію.

.org 511
end_boot:       .byte   0

В результаті скомпільований код буде займати рівно 512 байт, що дуже зручно для підготовки образу завантажувального диска.

3. Перші подихи ядра (head.S)

Ядро на жаль знову почнеться з асемблерні коду. Але тепер його буде зовсім небагато.

Ми власне задамо правильні значення сегментів для даних (ES, DS, FS, GS). Записавши туди значення відповідного дескриптора даних.

  cld
  cli
  movl $(__KERNEL_DS),%eax
  movl %ax,%ds
  movl %ax,%es
  movl %ax,%fs
  movl %ax,%gs

Перевіримо, чи нормально включилася адресна лінія A20 простим тестом запису. Обнулив для чистоти експерименту регістр прапорів.

     xorl %eax,%eax
1:   incl %eax
     movl %eax,0x000000
     cmpl %eax,0x100000
     je 1b
     pushl $0
     popfl

Викличемо довгоочікувану функцію, уже написану на С.

   call SYMBOL_NAME(start_my_kernel)

І більше нам тут робити нічого.

inf:    jmp     inf

4. Поговоримо на мові високого рівня (start.c)

Ось тепер ми повернулися до того з чого починали розповідь. Майже повернулися, тому що printf () тепер треба робити вручну. оскільки готових переривань вже немає, то будемо використовувати пряму запис в відеопам’ять. Для цікавих – майже весь код цій частині, з незначними змінами, повзаімствован з частини ядра Linux, здійснює розпакування (/ arch/i386/boot/compressed / *). Для складання вам буде потрібно додатково визначити такі макроси як inb (), outb (), inb_p (), outb_p (). Готові визначення найпростіше позичити з будь-якої версії Linux.

Тепер, щоб не плутатися з вбудованими в glibc функціями, скасуємо їх визначення

#undef memcpy

Задамо декілька своїх

static void puts(const char *); static char * vidmem = (char *) 0xb8000; / * адреса відеопаматі * / static int vidport; / * відеопорт * / static int lines, cols; / * кількість ліній і рядків на екран * / static int curr_x, curr_y; / * поточне положення курсору * /

І почнемо, нарешті, писати код на мові високого рівня … правда з невеликими асемблерними вставками.

/ * Функція перекладу курсору в положення (x, y). Робота ведеться через введення / виведення в відеопорт * /


void gotoxy(int x, int y)
{
int pos;
pos = (x + cols * y) * 2;
outb_p(14, vidport);
outb_p(0xff & (pos >> 9), vidport+1);
outb_p(15, vidport);
outb_p(0xff & (pos >> 1), vidport+1);
}

/ * Функція прокручування екрану. Працює, використовуючи пряму запис в відеопам’ять * /

static void scroll()
{
   int i;
   memcpy ( vidmem, vidmem + cols * 2, ( lines - 1 ) * cols * 2 );
   for ( i = ( lines - 1 ) * cols * 2; i < lines * cols * 2; i += 2 )
           vidmem[i] = ' ';
}

/ * Функція виведення рядка на екран * /

static void puts(const char *s)
{
  int x,y;
  char c;
  x = curr_x;
  y = curr_y;
  while ( ( c = *s++ ) != '\0' ) {
   if ( c == '\n' ) {
     x = 0;
     if ( ++y >= lines ) {
             scroll();
             y--;
     }
   } else {
     vidmem [ ( x + cols * y ) * 2 ] = c;
     if ( ++x >= cols ) {
          x = 0;
          if ( ++y >= lines ) {
            scroll();
                  y--;
          }
     }
 }
  }
  gotoxy(x,y);
}

/ * Функція копіювання з однієї області пам’яті в іншу. Заступник стандартної функції glibc * /

void* memcpy(void* __dest, __const void* __src,
                            unsigned int __n)
{
        int i;
        char *d = (char *)__dest, *s = (char *)__src;
        for (i=0;i<__n;i++) d[i] = s[i];
}

/ * Функція видає довгий і протяжних звук. Використовує тільки введення / виведення в порти тому дуже корисна для налагодження * /

make_sound()
{
__asm__("
   movb    $0xB6, %al\n\t
   outb    %al, $0x43\n\t
   movb    $0x0D, %al\n\t
   outb    %al, $0x42\n\t
   movb    $0x11, %al\n\t
   outb     %al, $0x42\n\t
   inb     $0x61, %al\n\t
   orb     $3, %al\n\t
   outb    %al, $0x61\n\t
");
} / * А ось і основна функція * /
int start_my_kernel()
{ / * Задаються основні параметри * /
   vidmem = (char *) 0xb8000;
   vidport = 0x3d4;
   lines = 25;
   cols = 80; / * Зчитується завбачливо збережені координати курсора * /
   curr_x=*(unsigned char *)(0x8000);
   curr_y=*(unsigned char *)(0x8001); / * Виводиться рядок * /
   puts("done\n"); / * Йдемо в нескінченний цикл * /
   while(1);
}

От і вивели ми цей “Hello World” на екран. Скільки зроблено роботи, а на екрані тільки два рядки

Booting data ...done Go to proteсted mode ... done

Небагато, але й немало. Закричала нова операційна система. Світ з радістю сприйняв її. Хто знає, може бути це новий Linux …

5. Підготовка завантажувального образу (floppy.img)

Отже, підготуємо завантажувальний образ нашої сістемкі.

Для початку зберемо завантажувальний сектор.


as86 -0 -a -o boot.o boot.S
ld86 -0 -s -o boot.img boot.o

Обріжемо 32 бітний заголовок і отримаємо таким чином чистий двійковий код.

dd if=boot.img of=boot.bin bs=32 skip=1

Зберемо ядро

gcc -traditional -c head.S -o head.o
gcc -O2 -DSTDC_HEADERS -c start.c

При компонуванні НЕ ЗАБУДБЬТЕ параметр “-T” він вказує щодо якого зміщення вести розрахунки, в нашому випадку оскільки ядро ​​вантажиться по адресy 0x1000, то і зсув соотетствующее

ld -m elf_i386 -Ttext 0x1000  -e startup_32 head.o start.o -o head.img

Очистимо зерна від плевел, тобто чистий двійковий код від всеческіх службових заголовків та коментарів

objcopy -O binary -R .note -R .comment -S head.img head.bin

І з’єднуємо воєдино завантажувальний сектор і ядро

cat boot.bin head.bin >floppy.img

Образ готовий. Записуємо на дискетка (заготовте кілька для експериментів, я прикінчив три штуки) перезавантажуємо комп’ютер і насолоджуємося.

cat floppy.img >/dev/fd0

6. Е-мое, що ж я зробив (…)

Здорово, правда? Приємно відчути себе майбутнім Торвальдсом або кимось ще. Червона лінія намічена, можна сміливо йти вперед, дописувати й переписувати систему. Описана процедура поки що єдина для безлічі операційних систем, будь то UNIX або Windows. Що напишете Ви? … не знає ніхто. Адже це буде Ваша система.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*