Оригинальный DVD-ROM: eXeL@B DVD !
eXeL@B ВИДЕОКУРС !

ВИДЕОКУРС ВЗЛОМ
выпущен 2 августа!


УЗНАТЬ БОЛЬШЕ >>
Домой | Статьи | RAR-cтатьи | FAQ | Форум | Скачать | Видеокурс
Новичку | Ссылки | Программирование | Интервью | Архив | Связь

ПРОГРАММИРОВАНИЕ НА C и С++



Возможности языков семейства Си по истине безграничны, однако, в этой свободе кроются и недостатки: всегда нужно программисту держать ухо востро и контроллировать "переполнение буфера", чтобы потом программа не вылетала в "синий экран" на массе разнообразных версий Windows и железа у пользователей. Те же крэкеры и реверсеры специально ищут в коде программ на Си уязвимости, куда можно подсадить любой вирусный код, об этом более подробно автор рассказывал в своём видеокурсе здесь. Я там многое узнал и теперь мой код стал значительно более безопасный.

The Real "Hello World"


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:0x7c00 и передаст туда управление. Мы его получим и для начала переместим себя пониже по адресу 0000:0x600, перейдем туда и спокойно продолжим работу. Собственно вся наша работа будет состоять из загрузки ядра (сектора 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:0x8000 ). Ядро позже заберет все данные и будет их использовать для вывода на экран победного сообщения.

 
 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. Что напишете Вы? ... не знает не кто. Ведь это будет Ваша система.



<< ВЕРНУТЬСЯ В ПОДРАЗДЕЛ

<< ВЕРНУТЬСЯ В ОГЛАВЛЕНИЕ




Материалы находятся на сайте https://exelab.ru/pro/



Оригинальный DVD-ROM: eXeL@B DVD !


Вы находитесь на EXELAB.rU
Проект ReactOS