Pull to refresh
43.4
Rating

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

НеоБИТ corporate blog System Programming *


В первой части нашей статьи мы рассказали о том, каким образом можно получить простую программу “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"
Tags: grubhowtobootgccgdbqemu
Hubs: НеоБИТ corporate blog System Programming
Total votes 124: ↑118 and ↓6 +112
Comments 9
Comments Comments 9

Top of the last 24 hours

Information

Location
Россия
Website
neobit.ru
Employees
51–100 employees
Registered