Как запустить программу без операционной системы: часть 2



    В первой части нашей статьи мы рассказали о том, каким образом можно получить простую программу “Hello World”, которая запускается без операционной системы и печатает сообщение на экран.

    В этой части статьи, хочется развить получившийся в первой части код таким образом, чтобы он мог быть отлажен через GDB, компилировался через оболочку Visual Studio и печатал на экран список PCI устройств.

    ! ВАЖНО!: Все дальнейшие действия могут успешно осуществляться только после успешного прохождения всех 6-ти шагов описанных в первой части статьи).


    Учимся отлаживать программу



    Основная статья: Использование отладчика GDB по максимуму

    Как отладить код kernel.bin? Для этого нужно добавить в kernel.bin симовлы для отладки и запустить отладчик:

    1. Добавим опцию компилятора в файле makefile, чтобы он генерировал отладочные символы:
    CFLAGS  = -Wall -fno-builtin -nostdinc -nostdlib -ggdb3
    


    2. Добавим пару строк на этапе сборки, чтобы на диск записывался kernel.bin без символов (такой файл можно сделать при помощи утилиты strip). Для этого нужно исправить цель kernel.bin в makefile:
    kernel.bin: $(OBJFILES)
    	$(LD) -T linker.ld -o $@ $^
    	cp $@ $@.dbg 
    	strip $@
    

    тогда:
    kernel.bin – не содержит символы – его можно запускать;
    kernel.bin.dbg – содержит и символы и код – его можно скормить отладчику.

    3. Установим отладчик:
    sudo apt-get install cgdb
    


    4. Перекомпилируем программу:
    make clean
    make all
    sudo make image
    


    5. Запустим qemu с опцией ожидания отладчика:
    sudo qemu-system-i386 -s -S -hda hdd.img &
    


    6. Запустим отладчик c указанием файла с символами:
    cgdb kernel.bin.dbg
    


    7. В отладчике подключимся к qemu и поставим breakpoint сразу на функции main:
    (gdb) target remote localhost:1234
    (gdb) break main
    




    8. Попадаем в main и отлаживаем ее:
    (gdb) c
    (gdb) n
    (gdb) n
    




    Таким образом, получается мощный инструмент отладки. Этот способ будет работать для QEMU, а для того, чтобы отладить программу непосредственно на железе, необходимо подключить модуль отладчика к нашей программе – это мы рассмотрим в одной из следующих статей.

    Компиляция из Visual Studio



    Основная статья: Использование оболочки Visual Studio 2010 для компиляции проектов с помощью gcc в Linux

    Как работать с полученным кодом из Visual Studio? Следуя инструкциям в статье собираем проект Visual Studio, не создавая проект на Linux – он уже есть.

    1. Установим в системе ssh:
    sudo apt-get install ssh
    


    2. Располагаем исходники проекта с kernel.bin на shared directory для виртуальной машины.

    3. Устанавливаем утилиту plink в папку tools и проверяем ее работу.

    4. Создаем проект Visual Studio следуя инструкциям и получаем такое дерево:

    \proj\kernel.c
    \proj\loader.s
    \proj\common\printf.c
    \proj\common\screen.c
    \proj\include\printf.h
    \proj\include\stdarg.h
    \proj\include\screen.h
    \proj\include\types.h
    \proj\makefile
    \proj\linker.ld
    \proj\tools\plink.exe
    \proj\kernel\kernel.sln
    \proj\kernel\kernel.suo
    \proj\kernel\kernel.sdf
    \proj\kernel\vs\kernel.vcxproj
    \proj\kernel\vs\kernel.vcxproj.filters
    \proj\kernel\vs\make_vs.props


    5. Формируем файл ”\proj\kernel\vs\make_vs.props” так же по инструкции:

    <?xml version="1.0" encoding="utf-8"?>
    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    
      <PropertyGroup Label="RemoteBuildLocals">
        <RblFolder>proj</RblFolder>
        <RblIncludePath>$(SolutionDir)\include\</RblIncludePath>
        <RblExecute>sudo make image; sudo qemu-system-i386 -hda hdd.img</RblExecute>
      </PropertyGroup>
    
      <PropertyGroup Label="RemoteBuildSettings">
        <RbHost>192.168.1.8</RbHost>
        <RbUser>user</RbUser>
        <RbPassword>123456</RbPassword>
        <RbRoot> ~/Desktop/_habr</RbRoot>
      </PropertyGroup>
    
      <PropertyGroup Label="RemoteBuild">
        <RbToolArgs> -pw $(RbPassword) $(RbUser)%40$(RbHost) cd $(RbRoot); cd $(RblFolder);</RbToolArgs>
        <RbToolExe>$(SolutionDir)tools\plink -batch $(RbToolArgs)</RbToolExe>
        <RbBuildCmd>$(RbToolExe) make all</RbBuildCmd>
        <RbRebuildAllCmd>$(RbToolExe) make rebuild</RbRebuildAllCmd>
        <RbCleanCmd>$(RbToolExe) make cleanall</RbCleanCmd>
        <RbExecuteCmd>$(RbToolArgs) $(RblExecute)</RbExecuteCmd>
        <RbIncludePath>$(RblIncludePath)</RbIncludePath>
      </PropertyGroup>
    
      <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
        <NMakeBuildCommandLine>$(RbBuildCmd)</NMakeBuildCommandLine>
        <NMakeReBuildCommandLine>$(RbRebuildAllCmd)</NMakeReBuildCommandLine>
        <NMakeCleanCommandLine>$(RbCleanCmd)</NMakeCleanCommandLine>
        <IncludePath>$(RbIncludePath)</IncludePath>
        <LocalDebuggerCommand>$(SolutionDir) tools\plink</LocalDebuggerCommand>
        <LocalDebuggerCommandArguments>$(RbExecuteCmd)</LocalDebuggerCommandArguments>
      </PropertyGroup>
    
      <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
        <NMakeBuildCommandLine>$(RbBuildCmd)</NMakeBuildCommandLine>
        <NMakeReBuildCommandLine>$(RbRebuildAllCmd)</NMakeReBuildCommandLine>
        <NMakeCleanCommandLine>$(RbCleanCmd)</NMakeCleanCommandLine>
        <IncludePath>$(RbIncludePath)</IncludePath>
        <LocalDebuggerCommand>$(SolutionDir)tools\plink</LocalDebuggerCommand>
        <LocalDebuggerCommandArguments>$(RbExecuteCmd)</LocalDebuggerCommandArguments>
      </PropertyGroup>
    
    </Project>
    


    6. Меняем файл ”\proj\kernel\vs\kernel.vcxproj ” так же по инструкции:
    <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
      <Import Project="$(SolutionDir)\vs\make_vs.props" />
      <ImportGroup Label="ExtensionSettings">
    
    

    7. В итоге должно получиться примерно следующее:



    8. Проверяем, что все работает:



    Таким образом, для дельнейшей разработки нашей программы можно использовать оболочку Visual Studio, хоть и компилятор GCC на Linux.

    Сканирование устройств PCI



    Основная статья: Как найти PCI устройства без операционной системы

    Как теперь просканировать системную шину PCI на наличие устройств? Следуя инструкциям в статье выполняем следующие действия (загрузочный образ уже готов, поэтому только добавляем код сканирования PCI):

    1. Добавляем в файл include\types.h, следующее определение типа:
    typedef unsigned long u32;
    typedef unsigned short u16;
    typedef unsigned char u8;
    


    2. Добавляем файл include\io.h, который без изменений можно взять из проекта bitvisor из каталога (include\io.h).

    3. Добавляем файл include\pci.h, который содержит основные определения для функций работы с PCI. Он имеет следующее содержимое:
    #ifndef _PCI_H
    #define _PCI_H
    
    #include "types.h"
    
    #define PCI_CONFIG_PORT      0x0CF8
    #define PCI_DATA_PORT        0x0CFC
    
    #define PCI_MAX_BUSES        255
    #define PCI_MAX_DEVICES      32
    #define PCI_MAX_FUNCTIONS    8
    
    #define PCI_HEADERTYPE_NORMAL        0
    #define PCI_HEADERTYPE_BRIDGE        1
    #define PCI_HEADERTYPE_CARDBUS       2
    #define PCI_HEADERTYPE_MULTIFUNC     0x80
    
    typedef union 
    {
    	struct
    	{
    		u16 vendorID;
    		u16 deviceID;
    		u16 commandReg;
    		u16 statusReg;
    		u8 revisionID;
    		u8 progIF;
    		u8 subClassCode;
    		u8 classCode;
    		u8 cachelineSize;
    		u8 latency;
    		u8 headerType;
    		u8 BIST;
    	} __attribute__((packed)) option;
    	u32 header[4];
    } __attribute__((packed)) PCIDevHeader;
    
    void ReadConfig32(u32 bus, u32 dev, u32 func, u32 reg, u32 *data);
    char *GetPCIDevClassName(u32 class_code);
    void PCIScan();
    
    #endif
    


    4. Добавляем файл pci.c в корень проекта, со следующим содержимым (мы немного улучшили этот код по сравнению с основной статьей):

    #include "types.h"
    #include "printf.h"
    #include "io.h"
    #include "pci.h"
    
    typedef struct 
    {
    	u32 class_code;
    	char name[32];
    } PCIClassName;
    
    static PCIClassName g_PCIClassNames[] = 
    {
    	{ 0x00, "before PCI 2.0"},
    	{ 0x01, "disk controller"},
    	{ 0x02, "network interface"},
    	{ 0x03, "graphics adapter"},
    	{ 0x04, "multimedia controller"},
    	{ 0x05, "memory controller"},
    	{ 0x06, "bridge device"},
    	{ 0x07, "communication controller"},
    	{ 0x08, "system device"},
    	{ 0x09, "input device"},
    	{ 0x0a, "docking station"},
    	{ 0x0b, "CPU"},
    	{ 0x0c, "serial bus"},
    	{ 0x0d, "wireless controller"},
    	{ 0x0e, "intelligent I/O controller"},
    	{ 0x0f, "satellite controller"},
    	{ 0x10, "encryption controller"},
    	{ 0x11, "signal processing controller"},
    	{ 0xFF, "proprietary device"}
    };
    
    typedef union
    {
    	struct
    	{
    		u32 zero 		: 2;
    		u32 reg_num     : 6;
    		u32 func_num    : 3;
    		u32 dev_num     : 5;
    		u32 bus_num     : 8;
    		u32 reserved    : 7;
    		u32 enable_bit  : 1;
    	};
    	u32 val;
    } PCIConfigAddres;
    
    void ReadConfig32(u32 bus, u32 dev, u32 func, u32 reg, u32 *data)
    {
    	PCIConfigAddres addr;
    	
    	addr.val = 0;
    	addr.enable_bit = 1;
    	addr.reg_num =  reg;
    	addr.func_num = func;
    	addr.dev_num =  dev;
    	addr.bus_num =  bus;		
    
    	out32(PCI_CONFIG_PORT, addr.val);
    	in32(PCI_DATA_PORT, data);
    	return;
    }
    
    char *GetPCIDevClassName(u32 class_code)
    {
    	int i;
    	for (i = 0; i < sizeof(g_PCIClassNames)/sizeof(g_PCIClassNames[0]); i++)
    	{
    		if (g_PCIClassNames[i].class_code == class_code)
    			return g_PCIClassNames[i].name;
    	}
    	return NULL;
    }
    
    int ReadPCIDevHeader(u32 bus, u32 dev, u32 func, PCIDevHeader *p_pciDevice)
    {
    	int i;
    	
    	if (p_pciDevice == 0)
    		return 1;
    	
    	for (i = 0; i < sizeof(p_pciDevice->header)/sizeof(p_pciDevice->header[0]); i++)
    		ReadConfig32(bus, dev, func, i, &p_pciDevice->header[i]);
    		
    	if (p_pciDevice->option.vendorID == 0x0000 || 
    		p_pciDevice->option.vendorID == 0xffff ||
    		p_pciDevice->option.deviceID == 0xffff)
    		return 1;
    		
    	return 0;
    }
    
    void PrintPCIDevHeader(u32 bus, u32 dev, u32 func, PCIDevHeader *p_pciDevice)
    {
    	char *class_name;
    	
    	printf("bus=0x%x dev=0x%x func=0x%x venID=0x%x devID=0x%x",
    			bus, dev, func, p_pciDevice->option.vendorID, p_pciDevice->option.deviceID);
    			
    	class_name = GetPCIDevClassName(p_pciDevice->option.classCode);
    	if (class_name)
    		printf(" class_name=%s", class_name);
    		
    	printf("\n");
    }
    
    void PCIScan(void)
    {
    	int bus;
    	int dev;
    	
    	for (bus = 0; bus < PCI_MAX_BUSES; bus++)
    		for (dev = 0; dev < PCI_MAX_DEVICES; dev++)
    		{
    			u32 func = 0;
    			PCIDevHeader pci_device;
    			
    			if (ReadPCIDevHeader(bus, dev, func, &pci_device))
    				continue;
    				
    			PrintPCIDevHeader(bus, dev, func, &pci_device);
    			
    			if (pci_device.option.headerType & PCI_HEADERTYPE_MULTIFUNC)
    			{
    				for (func = 1; func < PCI_MAX_FUNCTIONS; func++)
    				{
    					if (ReadPCIDevHeader(bus, dev, func, &pci_device))
    						continue;
    					PrintPCIDevHeader(bus, dev, func, &pci_device);
    				}
    			}
    		}
    }
    
    


    5. Добавляем запуск сканирования PCI устройств в kernel.c:

    #include "printf.h"
    #include "screen.h"
    #include "types.h"
    #include "pci.h"
    
    void main()
    {   
        clear_screen();
        printf("\n>>> Hello World!\n");
        PCIScan();
    }
    


    6. Вносим необходимые изменения в makefile:
    
    OBJFILES = \
    	loader.o  \
    	common/printf.o  \
    	common/screen.o  \
    	pci.o  \
    	kernel.o
    


    7. Теперь можно пересобрать проект:
    make rebuild
    sudo make image
    


    8. Запускаем проект, чтобы убедиться, что все работает:
    sudo qemu-system-i386 -hda hdd.img
    




    Так мы получили список PCI устройств на компьютере. Это так же будет работать и на обычном компьютере, загрузившись с флешки.

    Пройдя все шаги в этой статье вы можете собственноручно разобраться во всем и увидеть работающую программу, которую можно полноценно отлаживать.

    Ссылки на следующие статьи цикла:
    "Как запустить программу без операционной системы: часть 3: Графика"
    "Как запустить программу без операционной системы: часть 4. Параллельные вычисления"
    "Как запустить программу без операционной системы: часть 5. Обращение к BIOS из ОС"
    "Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT"
    • +112
    • 60.3k
    • 9
    НеоБИТ
    104.84
    Company
    Share post

    Comments 9

      +6
      Вот и продолжение. Благодарю.
        +18
        Интересно, что почти этот же подход можно применить и без эмулятора на живой линукс системе с заоффлайненым ядром:
        загрузить ядро с дыркой в физ. памяти, например memmap=64M$0x19000000
        заоффлайнить ядро в линуксе с echo 0 > /sys/devices/system/cpu/cpu2/online
        скопировать бинарник в 0x19000000 физической памяти через /dev/mem,mmap. (в бинарник добавить настройку gdt,idt по вкусу)
        и небольшой загрузчик в соответствующий адрес (линкскрипт настроить соответственно),
        инициировать INIT/SIPIx2 через LAPIC адрес на ACPI ID заоффлайненого ядра (warning: ACPI ID не всегда = Linux kernel core ID)

        Общаться потом можно через шаренную физ. память и например mwait.
          +5
          Мсье знает толк.
            +7
            Мне это напомнило диалоги из House M.D
              0
              Ух ты, это же просто гениально!
              Правда, с приходом аппаратной виртуализации уже не так актуально. То же самое, в принципе может сделать гипервизор, но при этом он ещё и виртуализирует ком-порт, например. Можно будет аттачиться дебаггером. И не нужно танцы с бубном для загрузки в память и перезапуска остановленного ядра.

              Но вообще идея хороша.
              Кстати, нечто очень похожее сейчас используется в мобильных процессорах. Например в TI OMAP на паре Cortex A9 работает линукс, а на паре Cortex M3 запускается проприетарная Realtime OS, на которой бегают аппаратные медиа-кодеки. И общение как раз происходит через shared memmory + аппаратные mailbox.
                0
                Любой ARM с MMU это вполне может с одним ядром начиная с ARM9. Стартует реалтаймовое ядро и из-под него запускается паравиртуализированный линукс. Собственно так работают телефоны на андроиде у quallcom — есть реалтаймовость для bb и аппаратная защита проприетарного кода от линукса.
                  0
                  Узнаю знакомые признаки наличия микроядра L4. ;)
            • UFO just landed and posted this here
              • UFO just landed and posted this here

                Only users with full accounts can post comments. Log in, please.