Ядро зависает при очистке области экрана размером 80x25

Тема в разделе "WASM.BEGINNERS", создана пользователем Aoizora, 4 янв 2024.

  1. Aoizora

    Aoizora Active Member

    Публикаций:
    0
    Регистрация:
    29 янв 2017
    Сообщения:
    362
    Почему эмулятор qemu зависает на этой функции очистки экрана?

    Код (C++):
    1. size_t terminal_row;
    2. size_t terminal_column;
    3. uint8_t terminal_color;
    4. uint16_t* terminal_buffer;
    5.  
    6. static const size_t VGA_WIDTH = 80;
    7. static const size_t VGA_HEIGHT = 25;
    8.  
    9. static inline uint8_t vga_entry_color(Color fg, Color bg)
    10. {
    11.     return fg | bg << 4;
    12. }
    13.  
    14. static inline uint16_t vga_entry(unsigned char uc, uint8_t color)
    15. {
    16.     return (uint16_t) uc | (uint16_t) color << 8;
    17. }
    18.  
    19. void keTerminalInitialize()
    20. {
    21.     terminal_row = 0;
    22.     terminal_column = 0;
    23.     terminal_color = vga_entry_color(LightBlue, Black);
    24.     terminal_buffer = (uint16_t*) 0xB8000;
    25.  
    26.     for (size_t y = 0; y < VGA_HEIGHT; y++)
    27.     {
    28.         for (size_t x = 0; x < VGA_WIDTH - 1; x++)
    29.         {
    30.             const size_t index = y * VGA_WIDTH + x;
    31.             terminal_buffer[index] = vga_entry(' ', terminal_color);
    32.         }
    33.     }
    34. }
    Условие, при котором эмулятор зависает, выглядит так. Во внутреннем цикле

    Код (C++):
    1. for (size_t x = 0; x < VGA_WIDTH; x++)
    Но если сделать так, то ядро не зависает и на экран выводится корректное сообщение

    Код (C):
    1. for (size_t x = 0; x < VGA_WIDTH - 1; x++)
    Я думаю, здесь происходит выход за границы сегмента, но почему?
     
  2. comrade

    comrade Константин Ёпрст

    Публикаций:
    0
    Регистрация:
    16 сен 2002
    Сообщения:
    232
    Адрес:
    Russian Federation
    Почему переменная terminal_buffer типа uint16_t* а не uint8_t* ? Попробуйте сделать:
    Код (Text):
    1.  
    2. uint8_t* terminal_buffer;
    3. ...
    4. terminal_buffer = (uint8_t*) 0xB8000;
    5.  
     
  3. Aoizora

    Aoizora Active Member

    Публикаций:
    0
    Регистрация:
    29 янв 2017
    Сообщения:
    362

    Потому что записываю в этот буфер 2-байтовые значения: символ и цветовой байт

    Код (Text):
    1. static inline uint16_t vga_entry(unsigned char uc, uint8_t color)
    2. {
    3.     return (uint16_t) uc | (uint16_t) color << 8;
    4. }
     
    Win32Api нравится это.
  4. comrade

    comrade Константин Ёпрст

    Публикаций:
    0
    Регистрация:
    16 сен 2002
    Сообщения:
    232
    Адрес:
    Russian Federation
    Как компилируется код? В каком режиме процессор? Можете пожалуйста показать assembly listing?
     
  5. Aoizora

    Aoizora Active Member

    Публикаций:
    0
    Регистрация:
    29 янв 2017
    Сообщения:
    362
    Вот boot.s


    Код (ASM):
    1. bits 32
    2. section .text
    3. ; Multiboot spec
    4.     align 4
    5.     dd 0x1BADB002
    6.     dd 0x00
    7.     dd -(0x1BADB002 + 0x00)
    8.  
    9. global _start
    10. extern KernelMain
    11.  
    12. _start:
    13.     cli
    14.     mov esp, stack_space
    15.     call KernelMain
    16.     hlt
    17.  
    18. section .bss
    19. resb 8192
    20. stack_space:
    Компилируется код так: gcc -c -m32 -O2 -ffreestanding -fno-rtti -fno-exceptions -nostdlib -nodefaultlibs main.cpp string.cpp terminal.cpp
    Линкуется так: ld -m elf_i386 -T linker.ld boot.o main.o string.o terminal.o -o kernel
    --- Сообщение объединено, 5 янв 2024 ---
    Код (Text):
    1. OUTPUT_FORMAT(elf32-i386)
    2. ENTRY(_start)
    3. SECTIONS
    4. {
    5.     . = 0x100000;
    6.     .text : { *(.text) }
    7.     .data : { *(.data) }
    8.     .bss  : { *(.bss)  }
    9. }
    10.  
    linker.ld:
     
  6. Aoizora

    Aoizora Active Member

    Публикаций:
    0
    Регистрация:
    29 янв 2017
    Сообщения:
    362
    Запускаю на виртуальном процессоре qemu-system-x86_64 -kernel kernel
     
  7. Treant

    Treant Member

    Публикаций:
    0
    Регистрация:
    24 май 2009
    Сообщения:
    248
    вот это
    terminal_buffer[index] = vga_entry(' ', terminal_color);
    попробуйте поменять на
    *(uint16_t*)(0xB8000+index*2) = vga_entry(' ', terminal_color);
     
  8. comrade

    comrade Константин Ёпрст

    Публикаций:
    0
    Регистрация:
    16 сен 2002
    Сообщения:
    232
    Адрес:
    Russian Federation
    Aoizora, я имел ввиду assembly listing главной части программы, которая пишет в 0xB8000.
     
  9. Aoizora

    Aoizora Active Member

    Публикаций:
    0
    Регистрация:
    29 янв 2017
    Сообщения:
    362
    Вот так выглядит листинг функции keWrite, которая записывает значение в видеобуфер:

    Код (ASM):
    1. 00000080 <_Z11keWriteBytechjj>:
    2.  
    3. 00000130 <_Z7keWritePKc>:
    4. push  edi
    5. push  esi
    6. xor  esi,esi
    7. push  ebx
    8. mov  edi,DWORD PTR [esp+0x10]
    9. call  13a <_Z7keWritePKc+0xa>
    10. add  ebx,0x2
    11. jmp  15e <_Z7keWritePKc+0x2e>
    12. lea  esi,[esi+eiz*1+0x0]
    13. lea  esi,[esi+0x0]
    14. movsx  eax,BYTE PTR [edi+esi*1]
    15. add  esi,0x1
    16. push  eax
    17. call  159 <_Z7keWritePKc+0x29>
    18. pop  eax
    19. sub  esp,0xc
    20. push  edi
    21. call  163 <_Z7keWritePKc+0x33>
    22. add  esp,0x10
    23.  cmp  esi,eax
    24. jb  150 <_Z7keWritePKc+0x20>
    25. pop  ebx
    26. pop  esi
    27. pop  edi
    28. ret
    29.  
    30.  
    31.  
    К сожалению, синтаксис асма GNU

    Код (C):
    1. void keWriteByte(char c, uint8_t color, size_t x, size_t y)
    2. {
    3.     terminal_buffer[y * VGA_WIDTH + x] = vga_entry(c, color);
    4. }
    --- Сообщение объединено, 7 янв 2024 ---
    Это не помогло. Вот мой код:

    Код (C):
    1.  
    2. #include "terminal.hpp"
    3. #include "string.hpp"
    4.  
    5. size_t terminal_row;
    6. size_t terminal_column;
    7. uint8_t terminal_color;
    8. uint16_t* terminal_buffer;
    9.  
    10. static const size_t VGA_WIDTH = 80;
    11. static const size_t VGA_HEIGHT = 25;
    12.  
    13. static inline uint8_t vga_entry_color(Color fg, Color bg)
    14. {
    15.    return fg | bg << 4;
    16. }
    17.  
    18. static inline uint16_t vga_entry(unsigned char uc, uint8_t color)
    19. {
    20.    return (uint16_t) uc | (uint16_t) color << 8;
    21. }
    22.  
    23. void keInitializeTerminal()
    24. {
    25.    terminal_row = 0;
    26.    terminal_column = 0;
    27.    terminal_color = vga_entry_color(LightBlue, Black);
    28.    terminal_buffer = (uint16_t*) 0xB8000;
    29.  
    30.    for (size_t y = 0; y < VGA_HEIGHT; y++)
    31.   {
    32.      for (size_t x = 0; x < VGA_WIDTH; x++)
    33.   {
    34.        const size_t index = y * VGA_WIDTH + x;
    35.        *(uint16_t *)(0xB8000 + index*2) = vga_entry(' ', terminal_color);
    36.      }
    37.    }
    38. }
    39.  
    40. void keWriteByte(char c, uint8_t color, size_t x, size_t y)
    41. {
    42.    const size_t index = y * VGA_WIDTH + x;
    43.    *(uint16_t *)(0xB8000 + index*2) = vga_entry(c, color);
    44. }
    45.  
    46. void keWriteByte(char c)
    47. {
    48.    keWriteByte(c, terminal_color, terminal_column, terminal_row);
    49.  
    50.    if (++terminal_column == VGA_WIDTH)
    51.    {
    52.      terminal_column = 0;
    53.      if (++terminal_row == VGA_HEIGHT)
    54.      {
    55.        terminal_row = 0;
    56.      }
    57.    }
    58. }
    59.  
    60. void keWrite(const char* s)
    61. {
    62.    for (size_t i = 0; i < CString::strlen(s); ++i)
    63.      keWriteByte(s[i]);
    64. }
    65.  
     
  10. Treant

    Treant Member

    Публикаций:
    0
    Регистрация:
    24 май 2009
    Сообщения:
    248
    Aoizora,
    скомпилил ваш код и запустил (32bit, paging mode):
    Код (C):
    1. size_t terminal_row;
    2. size_t terminal_column;
    3. uint8_t terminal_color;
    4. uint16_t* terminal_buffer;
    5. static const size_t VGA_WIDTH = 80;
    6. static const size_t VGA_HEIGHT = 25;
    7. static inline uint8_t vga_entry_color(uint8_t fg, uint8_t bg)
    8. {
    9.    return fg | bg << 4;
    10. }
    11. static inline uint16_t vga_entry(unsigned char uc, uint8_t color)
    12. {
    13.    return (uint16_t) uc | (uint16_t) color << 8;
    14. }
    15. void keInitializeTerminal()
    16. {
    17.    terminal_row = 0;
    18.    terminal_column = 0;
    19.    terminal_color = vga_entry_color(0x0f, 0x00);
    20.    terminal_buffer = (uint16_t*) VIDEO_MEMORY;
    21.    for (size_t y = 0; y < VGA_HEIGHT; y++)
    22.   {
    23.      for (size_t x = 0; x < VGA_WIDTH; x++)
    24.   {
    25.        const size_t index = y * VGA_WIDTH + x;
    26.        *(uint16_t *)(VIDEO_MEMORY + index*2) = vga_entry(' ', terminal_color);
    27.      }
    28.    }
    29. }
    отработало корректно, проблемы не с этим алгоритмом
     

    Вложения:

  11. Entropy

    Entropy Member

    Публикаций:
    0
    Регистрация:
    23 авг 2020
    Сообщения:
    185
    Aoizora, ELF файл запускается загрузчиком GRUB ?
     
  12. Aoizora

    Aoizora Active Member

    Публикаций:
    0
    Регистрация:
    29 янв 2017
    Сообщения:
    362
    Я не пробовал, запускаю только на qemu
    Как можно запустить мое ядро на Virtual Box? Как туда установить grub?
     
  13. Treant

    Treant Member

    Публикаций:
    0
    Регистрация:
    24 май 2009
    Сообщения:
    248
    Aoizora,
    создайте flat диск на VMWare
    получите 2 файла, один описывает диск, а второй - flat представление диска
    вот это flat представление вам и нужно генерить, FASM'ом например
    вот пример для 2MiB диска:
    Код (ASM):
    1. format binary as 'vmdk'
    2. file '../build/MBR.bin'
    3. file '../build/BootSector.bin'
    4. file '../build/Kernel.bin'
    5. times 2048*1024-($-$$) db 00h
    Потом можно этот диск подключить к VirtualBox
     
  14. HoShiMin

    HoShiMin Well-Known Member

    Публикаций:
    5
    Регистрация:
    17 дек 2016
    Сообщения:
    1.455
    Адрес:
    Россия, Нижний Новгород
    Минутка безумия.

    Madness.gif

    Код (C++):
    1. /* main.cpp */
    2. namespace
    3. {
    4.  
    5. namespace Vga
    6. {
    7.     struct Cell
    8.     {
    9.         char sym;
    10.         unsigned char color;
    11.     };
    12.     static_assert(sizeof(Cell) == sizeof(unsigned short));
    13.  
    14.     static constexpr unsigned int addr = 0xB8000;
    15.  
    16.     static constexpr unsigned char width = 80;
    17.     static constexpr unsigned char height = 25;
    18.  
    19.     using Screen = Cell[height][width];
    20.  
    21.     Screen& screen() noexcept
    22.     {
    23.         return *reinterpret_cast<Screen*>(addr);
    24.     }
    25. }
    26.  
    27. } // namespace
    28.  
    29. extern "C" void kernelMain()
    30. {
    31.     auto& screen = Vga::screen();
    32.  
    33.     unsigned int counter = 0;
    34.     while (true)
    35.     {
    36.         for (auto y = 0; y < Vga::height; ++y)
    37.         {
    38.             for (auto x = 0; x < Vga::width; ++x)
    39.             {
    40.                 auto& cell = screen[y][x];
    41.                 cell.sym = (counter + x) % 0xFF;
    42.                 cell.color = (counter + x) & 0xFF;
    43.             }
    44.         }
    45.  
    46.         ++counter;
    47.     }
    48. }
    Код (ASM):
    1. ; boot.S
    2. bits 32
    3. section .text
    4. ; Multiboot spec
    5.     align 4
    6.     dd 0x1BADB002
    7.     dd 0x00
    8.     dd -(0x1BADB002 + 0x00)
    9.  
    10. global _start
    11. extern kernelMain
    12.  
    13. _start:
    14.     cli
    15.     mov esp, stackSpace
    16.     call kernelMain
    17.     hlt
    18. section .bss
    19. resb 8192
    20. stackSpace:
    Код (Bash):
    1. #!/bin/bash
    2. nasm -f elf ./boot.S
    3. g++ -Wall -Wpedantic -c -m32 -O2 -ffreestanding -fno-rtti -fno-exceptions -nostdlib -nodefaultlibs ./main.cpp -o ./main.o
    4. ld -m elf_i386 -T linker.ld boot.o main.o -o kernel
    Код (Text):
    1. qemu-system-x86_64.exe -kernel .\kernel

    Кстати, UbIvItS, что там насчёт того, что для ЕдРа подходит только си?)
     
    Treant нравится это.
  15. Aoizora

    Aoizora Active Member

    Публикаций:
    0
    Регистрация:
    29 янв 2017
    Сообщения:
    362
    Офигенно. Научите так писать ядерный код на C++ :) Какие книги для этого почитать?
     
  16. HoShiMin

    HoShiMin Well-Known Member

    Публикаций:
    5
    Регистрация:
    17 дек 2016
    Сообщения:
    1.455
    Адрес:
    Россия, Нижний Новгород
    Да никаких, просто берёшь и пишешь)
    Единственное ограничение - в ядре у тебя не будет исключений. В остальном можешь юзать все фичи.
    Даже коллекции, типа std::vector, если определишь операторы new/delete (но коллекции лучше написать свои, т.к. исключений нет, и, если вылетит OutOfMemory, потащит за собой и всё ядро).
     
  17. Aoizora

    Aoizora Active Member

    Публикаций:
    0
    Регистрация:
    29 янв 2017
    Сообщения:
    362
    А как писать системные сервисы, которые можно было бы вызывать из юзерспейс кода? Можно примеры?
     
  18. HoShiMin

    HoShiMin Well-Known Member

    Публикаций:
    5
    Регистрация:
    17 дек 2016
    Сообщения:
    1.455
    Адрес:
    Россия, Нижний Новгород
    Зависит от того, работает ли твоё ядро в x32 или ты уже переключил его в x64.

    Для начала, надо понять, как устроено разделение на юзермод и ядро.
    В 32х-битном защищённом режиме вся память разделена на сегменты (регионы), каждый из которых описывается «дескриптором» - структуркой, хранящей начальный адрес региона, размер, а также уровень привилегий, на котором ты можешь получить доступ к этому региону.
    Дескрипторы хранятся в таблицах дескрипторов, про которые знает процессор. Этих таблиц может быть несколько: одна таблица глобальных дескрипторов, которая описывает регионы, видимые всем задачам - она называется GDT (Global Descriptor Table); и несколько таблиц дескрипторов, описывающих сегменты для каждой задачи (по таблице на задачу) - они называются LDT (Local Descriptor Table).
    Под задачами подразумеваются потоки, которыми управляет аппаратный планировщик. Эта технология не используется ни в Windows, ни в Linux: вместо него используют программное переключение потоков, программируя контроллер прерываний, чтобы тот срабатывал по таймеру, и в обработчике прерываний вручную переключают потоки.

    Так вот, у нас есть таблицы с дескрипторами, описывающими начальные адреса, размеры и уровни привилегий для сегментов.
    Процессор в защищённом режиме в x32 адресует память всегда относительно сегментов: обращения к стеку идут через сегмент стека, обращения на чтение и запись вне стека - через сегмент данных, а выполнение кода - через сегмент кода.
    Каждый из этих сегментов настраивается через специальные регистры-селекторы, хранящие номер сегмента в таблице дескрипторов (в глобальной GDT или в LDT для текущей задачи).
    Всего в процессоре шесть регистров-селекторов (их также называют сегментными регистрами), каждый настраивает сегмент для определённых задач:
    CS - селектор сегмента для кода.
    DS - селектор сегмента данных.
    SS - селектор сегмента для стека.
    ES, FS, GS - разработчик системы сам придумывает для них назначение.

    Каждый сегментный регистр занимает 16 бит и представляет собой битовую структурку такого вида:
    Код (C++):
    1. struct SegmentSelector
    2. {
    3.     unsigned short rpl : 2; // Requested Privilege Level.
    4.     unsigned short ti : 1; // Table Indicator: 0 = GDT, 1 = LDT.
    5.     unsigned short index : 13; // Index in the corresponding table
    6. };
    Для сегмента кода поле RPL в селекторе CS является текущим уровнем привилегий (CPL, Current Privilege Level) и определяет, на каком уровне привилегий сейчас работает процессор.
    Иными словами, если два младших бита в CS равны нулю, ты в Ring0, если равны трём (11 в двоичном виде) - ты в Ring3.

    Дело за малым: нужно описать два сегмента - один для юзермода, другой для ядра. Оба положить в GDT и назначить CS/DS/DS на один из этих двух сегментов.
    Например, можно сделать юзермодный сегмент в нижних двух гигабайтах, памяти, а ядро - в верхних двух.

    Переключение между сегментами кода выполняется через межсегментные вызовы или прыжки. Но переключиться из сегмента с более низкими привилегиями (например, из Ring3) в сегмент с более высокими (Ring0) через межсегментный прыжок не получится: не дадут проверки доступа.
    Для переключений на более высокий уровень привилегий существует несколько способов: сгенерировать прерывание, вызвать одну из инструкций для системных вызовов (sysenter на x32 или syscall на x64) или создать в GDT/LDT специальный шлюз - дескриптор, через который разрешено поднимать привилегии межсегментными переходами.

    Если ядро 32х-битное, следует использовать инструкцию sysenter.
    При её вызове процессор переключает CS на селектор, заданный в MSR-регистре IA32_SYSENTER_CS, а также переключает EIP и ESP на ядерные, заданные в MSR-регистрах IA32_SYSENTER_EIP (здесь адрес обработчика всех сисколлов) и IA32_SYSENTER_ESP (здесь адрес буфера под ядерный стек).
    Подробности в Intel SDM или здесь: https://www.felixcloutier.com/x86/sysenter

    Если ядро 64х-битное, используется инструкция syscall, которая загружает в CS содержимое регистра IA32_STAR (Syscall TARget), а в RIP - IA32_LSTAR (Long-mode Syscall TARget) или IA32_CSTAR (Compatibility-mode Syscall TARget).
    Подробности здесь:
    https://www.felixcloutier.com/x86/syscall

    Про всё это в деталях можно ознакомиться в спецификациях Intel и AMD, там есть отдельные главы, где всё разобрано понятным языком с картинками и диаграммами.

    По описанному выше почитай главы в Intel SDM, Volume 3 (System Programming Guide):
    Chapter 3 - Protected-Mode Memory Management
    Chapter 4 - Paging
    Chapter 5 - Protection (в частности, 5.8 - Privilege Level Checking When Transferring Program Control Between Code Segments).

    Если понадобятся хедеры для всех системных структурок, можешь найти их здесь: https://github.com/HoShiMin/Arch
     
  19. Aoizora

    Aoizora Active Member

    Публикаций:
    0
    Регистрация:
    29 янв 2017
    Сообщения:
    362
    Понятно, мне пока это будет сложно реализовать.

    Я смотрю курс по написанию ОС, но там все примеры на GNU ассемблере. https://stepik.org/lesson/395524/step/12?unit=384513

    А я переписываю их на NASM. У меня загрузка БИОС получилась так:
    Код (ASM):
    1.  
    2. [BITS 16]
    3. [ORG 0x7C00]
    4.  
    5. start:
    6.   jmp 0:real_start
    7.  
    8. real_start:
    9.   xor ax, ax
    10.   mov ds, ax
    11.   mov ss, ax
    12.   mov sp, 0x7C00
    13.   add sp, 0x0400
    14.  
    15.   mov ax, 0xB800
    16.   mov es, ax
    17.   mov si, msg
    18.   xor di, di
    19.   mov cx, 5
    20.   call memcpy
    21.  
    22. hang:
    23.   jmp hang
    24.  
    25. memcpy:
    26.   test cx, cx
    27.   jz out
    28. again:
    29.   mov ah, [si]
    30.   mov es:[di], ah
    31.   inc si
    32.   inc di
    33.   dec cx
    34.   jnz again
    35. out:
    36.   ret
    37.  
    38. msg db "Hello",0
    39.  
    40. times 510 - ($ - $$) db 0
    41.  
    42. db 0x55
    43. db 0xAA
    44.  
    Но при запуске в qemu на экран выводится какой-то мусор:

    >qemu-system-i386 -drive file=boot,index=0,media=disk,format=raw

    [​IMG]


    Я подключился к qemu при помощи gdb для удаленной отладки, выставил set assembly-flavor intel и сделал окно layout asm, получился такой вывод:

    [​IMG]


    Почему код в gdb отличается код кода в исходнике? Например, инструкции xor ax, ax в начале кода в gdb вообще нет, и там местами все остальное тоже по-другому.

    Вот мейкфайл, которым я компилирую


    Код (Text):
    1. all: boot
    2.  
    3. boot: boot.o linker.ld
    4.         ld -T linker.ld boot.o -o boot
    5.  
    6. boot.o: boot.s
    7.         nasm boot.s -o boot
    8.  
    9. clean:
    10.         rm *.o boot
    11.  
    --- Сообщение объединено, 9 янв 2024 ---
    Вот мой скрипт для запуска дебага

    Код (Text):
    1. qemu-system-i386 -hda boot -S -s &
    2. gdb -ex 'target remote localhost:1234' \
    3.     -ex 'set architecture i8086' \
    4.     -ex 'break *0x7c00' \
    5.     -ex 'continue'
    6.  
     
  20. HoShiMin

    HoShiMin Well-Known Member

    Публикаций:
    5
    Регистрация:
    17 дек 2016
    Сообщения:
    1.455
    Адрес:
    Россия, Нижний Новгород
    Он выводит не мусор, а каждую чётную букву (‘H’, ‘l’ и ‘o’ из слова «Hello»): буфер ожидает, что в него будут писать в том же формате, что и в твоём 32х-битном примере выше: байт символа, затем байт цвета.
    У тебя получается, что на символы ложатся буквы Hlo, а буквы el и нуль-терминатор интерпретируются как цвет.
     
    Aoizora нравится это.