Pull to refresh

ОС с нуля: Глава 2, Часть 1 — Да зачем нам этот Legacy

Reading time9 min
Views17K

Введение

Пару месяцев назад я решил начать серию статей про написание своей ОС с нуля. Описал написание Legacy MBR загрузчика и переход в защищенный режим (без прерываний) и ещё пару мелочей. Сегодня я решил, что попытаюсь "перезапустить" эту серию (сохранив нумерацию частей). Суть в том, что теперь будут использоваться актуальные на август 2022 года материалы, и разработанное ПО можно будет легко протестировать на своей (U)EFI-машине.

Ассемблер - всему голова!

Не знаю как с этим у других людей, но лично я влюблен в ассемблер. При этом, переходил я на него, предварительно изучив Python, а не С или Фортран. В общем, программировать я буду всё ещё на ассемблере, конкретно - fasm, так как у него очень мощный препроцессор и компоновщик встроен в компилятор.

Загрузчики бывают разные...

Я назову 2 основных: это Legacy, или же MBR, и Secure, или же EFI-загрузчики (надеюсь, с терминами не напутал). В первой части я писал двух-ступенчатый загрузчик: MBR и фиксированно-расположенный SSB (Second-Stage Bootloader, загрузчик 2 стадии). В этот раз будем писать EFI-загрузчик.

Как загружаются UEFI-загрузчики?

Ну, примерно так: запускается биос (вроде с F000:FFFF или где-то там), производит POST (Power On Self Test), если все ОК, то запускает (U)EFI. UEFI в свою очередь ищет диски с известными файловыми системами (ntfs, brtfs, extFAT, FAT32, isofs, cdfs, ext2/3/4, есть ещё, но это уже опционально), ищет на них по адрессу /EFI/BOOT/ файл с расширениет *.efi , который называется bootX.efi где Х это платформа, для которой написан загрузчик. В целом, это всё. (ах, да, чуть не забыл: UEFI32 запускают bootia32.efi в защищенном режиме, а UEFI64 запускают bootx64.efi в долгом режиме).

Что вообще такое efi-приложение?

Технически, это тот же самый PE (Portable Excutable, формат исполняемых файлов для Microsoft Windows, *.exe, *.msi, *.dll, *.sys, *.mui), он даже разбирается с помощью IDA!

Приступим!

Для начала, просто exe-шник не подойдет. в строку с format нужно писать нечто подобное:

format PE64 DLL EFI

Потом, будет ну прям ОЧЕНЬ (на самом деле не ПРЯМ очень, но будет) сложно писать efi-app без библиотек. поэтому я набросал свою (листинг 1, 2, 3).

Листинг 1 (sysuefi.inc) - некоторые структуры, макросы и функции, без которых жизнь медом не покажется.

Листинг 2 (libuefi.inc) - некоторые полезные функции, начинаются с __, потому, что будут обернуты в Листинге 3

Листинг 3 (macroefi.inc) - обертка над Листингами 1 и 2, полностью из макросов и структур.

Также рекомендую использование такой вещи как struct.inc, гуляющей по всем интернетам. В библиотечках я её не использовал, но в будущем будет удобно свои структурки определять.

В итоге, получается вот-такая файловая система у нашего "проекта" (рис. 1)

рис. 1: Файловая система проекта
рис. 1: Файловая система проекта
листинг 1
; sysuefi.inc for fasm assembly ;

struc int8 {
  align 1
  . db ?
}
struc int16 {
  align 2
  . dw ?
  align 1
}
struc int32 {
  align 4
  . dd ?
  align 1
}
struc int64 {
  align 8
  . dq ?
  align 1
}
struc intn {
  align 8
  . dq ?
  align 1
}
struc dptr {
  align 8
  . dq ?
  align 1
}
 
;symbols
 
EFIERR = 0x8000000000000000
EFI_SUCCESS			= 0
EFI_LOAD_ERROR			= EFIERR or 1
EFI_INVALID_PARAMETER		= EFIERR or 2
EFI_UNSUPPORTED 		= EFIERR or 3
EFI_BAD_BUFFER_SIZE		= EFIERR or 4
EFI_BUFFER_TOO_SMALL		= EFIERR or 5
EFI_NOT_READY			= EFIERR or 6
EFI_DEVICE_ERROR		= EFIERR or 7
EFI_WRITE_PROTECTED		= EFIERR or 8
EFI_OUT_OF_RESOURCES		= EFIERR or 9
EFI_VOLUME_CORRUPTED		= EFIERR or 10
EFI_VOLUME_FULL 		= EFIERR or 11
EFI_NO_MEDIA			= EFIERR or 12
EFI_MEDIA_CHANGED		= EFIERR or 13
EFI_NOT_FOUND			= EFIERR or 14
EFI_ACCESS_DENIED		= EFIERR or 15
EFI_NO_RESPONSE 		= EFIERR or 16
EFI_NO_MAPPING			= EFIERR or 17
EFI_TIMEOUT			= EFIERR or 18
EFI_NOT_STARTED 		= EFIERR or 19
EFI_ALREADY_STARTED		= EFIERR or 20
EFI_ABORTED			= EFIERR or 21
EFI_ICMP_ERROR			= EFIERR or 22
EFI_TFTP_ERROR			= EFIERR or 23
EFI_PROTOCOL_ERROR		= EFIERR or 24
 
macro structure name
{
  virtual at 0
    name name
  end virtual
}
 
;structureures
EFI_SYSTEM_TABLE_SIGNATURE	equ	49h,42h,49h,20h,53h,59h,53h,54h
struc EFI_TABLE_HEADER {
 .Signature    		int64
 .Revision     		int32
 .HeaderSize   		int32
 .CRC32        		int32
 .Reserved     		int32
}
structure EFI_TABLE_HEADER
 
struc EFI_SYSTEM_TABLE {
 .Hdr		        EFI_TABLE_HEADER
 .FirmwareVendor        dptr
 .FirmwareRevision      int32
 .ConsoleInHandle       dptr
 .ConIn 	        dptr
 .ConsoleOutHandle      dptr
 .ConOut	        dptr
 .StandardErrorHandle   dptr
 .StdErr	        dptr
 .RuntimeServices       dptr
 .BootServices	        dptr
 .NumberOfTableEntries  intn
 .ConfigurationTable    dptr
}
structure EFI_SYSTEM_TABLE
 
struc SIMPLE_TEXT_OUTPUT_INTERFACE {
 .Reset 	    	dptr
 .OutputString	    	dptr
 .TestString	    	dptr
 .QueryMode	    	dptr
 .SetMode	    	dptr
 .SetAttribute	    	dptr
 .ClearScreen	    	dptr
 .SetCursorPosition 	dptr
 .EnableCursor	    	dptr
 .Mode		    	dptr
}
structure SIMPLE_TEXT_OUTPUT_INTERFACE
 
;---include ends
 
struc SIMPLE_INPUT_INTERFACE {
 .Reset			dptr
 .ReadKeyStroke		dptr
 .WaitForKey		dptr
}
structure SIMPLE_INPUT_INTERFACE
 
struc EFI_BOOT_SERVICES_TABLE {
 .Hdr		       	EFI_TABLE_HEADER
 .RaisePriority		dptr
 .RestorePriority	dptr
 .AllocatePages		dptr
 .FreePages		dptr
 .GetMemoryMap		dptr
 .AllocatePool		dptr
 .FreePool		dptr
 .CreateEvent		dptr
 .SetTimer		dptr
 .WaitForEvent		dptr
 .SignalEvent		dptr
 .CloseEvent		dptr
 .CheckEvent		dptr
 .InstallProtocolInterface dptr
 .ReInstallProtocolInterface dptr
 .UnInstallProtocolInterface dptr
 .HandleProtocol	dptr
 .Void			dptr
 .RegisterProtocolNotify dptr
 .LocateHandle		dptr
 .LocateDevicePath	dptr
 .InstallConfigurationTable dptr
 .ImageLoad		dptr
 .ImageStart		dptr
 .Exit			dptr
 .ImageUnLoad		dptr
 .ExitBootServices	dptr
 .GetNextMonotonicCount	dptr
 .Stall			dptr
 .SetWatchdogTimer	dptr
 .ConnectController	dptr
 .DisConnectController	dptr
 .OpenProtocol		dptr
 .CloseProtocol		dptr
 .OpenProtocolInformation dptr
 .ProtocolsPerHandle	dptr
 .LocateHandleBuffer	dptr
 .LocateProtocol	dptr
 .InstallMultipleProtocolInterfaces dptr
 .UnInstallMultipleProtocolInterfaces dptr
 .CalculateCrc32	dptr
 .CopyMem		dptr
 .SetMem		dptr
}
structure EFI_BOOT_SERVICES_TABLE
 
struc EFI_RUNTIME_SERVICES_TABLE {
 .Hdr		       	EFI_TABLE_HEADER
 .GetTime		dptr
 .SetTime		dptr
 .GetWakeUpTime		dptr
 .SetWakeUpTime		dptr
 .SetVirtualAddressMap	dptr
 .ConvertPointer	dptr
 .GetVariable		dptr
 .GetNextVariableName	dptr
 .SetVariable		dptr
 .GetNextHighMonoCount	dptr
 .ResetSystem		dptr
}
structure EFI_RUNTIME_SERVICES_TABLE
 
struc EFI_TIME {
 .Year			int16
 .Month			int8
 .Day			int8
 .Hour			int8
 .Minute		int8
 .Second		int8
 .Pad1			int8
 .Nanosecond		int32
 .TimeZone		int16
 .Daylight		int8
 .Pad2			int8
 .sizeof		rb 1
}
structure EFI_TIME
 
EFI_LOADED_IMAGE_PROTOCOL_UUID equ 0A1h,31h,1bh,5bh,62h,95h,0d2h,11h,8Eh,3Fh,0h,0A0h,0C9h,69h,72h,3Bh
struc EFI_LOADED_IMAGE_PROTOCOL {
 .Revision		int32
 .ParentHandle		int64
 .SystemTable		dptr
 .DeviceHandle		int64
 .FilePath		dptr
 .Reserved		int64
 .LoadOptionsSize	int32
 .ImageBase		dptr
 .ImageSize		int64
 .ImageCodeType		int32
 .ImageDataType		int32
 .UnLoad		dptr
}
structure EFI_LOADED_IMAGE_PROTOCOL
 
EFI_BLOCK_IO_PROTOCOL_UUID equ 21h,5bh,4eh,96h,59h,64h,0d2h,11h,8eh,39h,00h,0a0h,0c9h,69h,72h,3bh
struc EFI_BLOCK_IO_PROTOCOL {
 .Revision		int64
 .Media			dptr
 .Reset			dptr
 .ReadBlocks		dptr
 .WriteBlocks		dptr
 .FlushBlocks		dptr
}
structure EFI_BLOCK_IO_PROTOCOL
 
struc EFI_BLOCK_IO_MEDIA {
 .MediaId		int32
 .RemovableMedia	int8
 .MediaPresent		int8
 .LogicalPartition	int8
 .ReadOnly		int8
 .WriteCaching		int8
 .BlockSize		int32
 .IoAlign		int32
 .LastBlock		int64
}
structure EFI_BLOCK_IO_MEDIA
 
EFI_GRAPHICS_OUTPUT_PROTOCOL_UUID equ 0deh, 0a9h, 42h,90h,0dch,023h,38h,04ah,96h,0fbh,7ah,0deh,0d0h,80h,51h,6ah
struc EFI_GRAPHICS_OUTPUT_PROTOCOL {
 .QueryMode		dptr
 .SetMode		dptr
 .Blt			dptr
 .Mode			dptr
}
structure EFI_GRAPHICS_OUTPUT_PROTOCOL
 
struc EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE {
 .MaxMode		int32
 .CurrentMode		int32
 .ModeInfo		dptr
 .SizeOfModeInfo	intn
 .FrameBufferBase	dptr
 .FrameBufferSize	intn
}
structure EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE
 
struc EFI_GRAPHICS_OUTPUT_MODE_INFORMATION {
 .Version		int32
 .HorizontalResolution	int32
 .VerticalResolution	int32
 .PixelFormat		int32
 .RedMask		int32
 .GreenMask		int32
 .BlueMask		int32
 .Reserved		int32
 .PixelsPerScanline	int32
}
structure EFI_GRAPHICS_OUTPUT_MODE_INFORMATION

macro InitializeLib
{
		clc
		or			rdx, rdx
		jz			.badout
		cmp			dword [rdx], 20494249h
		je			@f
.badout:	  xor			rcx, rcx
		xor			rdx, rdx
		stc
@@:		mov			[efi_handler], rcx
		mov			[efi_ptr], rdx
}

macro uefi_call_wrapper			interface,function,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11
{
numarg = 0
if ~ arg11 eq
 numarg = numarg + 1
 if ~ arg11 eq rdi
		mov			rdi, arg11
 end if
end if
if ~ arg10 eq
 numarg = numarg + 1
 if ~ arg10 eq rsi
		mov			rsi, arg10
 end if
end if
if ~ arg9 eq
 numarg = numarg + 1
 if ~ arg9 eq r14
		mov			r14, arg9
 end if
end if
if ~ arg8 eq
 numarg = numarg + 1
 if ~ arg8 eq r13
		mov			r13, arg8
 end if
end if
if ~ arg7 eq
 numarg = numarg + 1
 if ~ arg7 eq r12
		mov			r12, arg7
 end if
end if
if ~ arg6 eq
 numarg = numarg + 1
 if ~ arg6 eq r11
		mov			r11, arg6
 end if
end if
if ~ arg5 eq
 numarg = numarg + 1
 if ~ arg5 eq r10
		mov			r10, arg5
 end if
end if
if ~ arg4 eq
 numarg = numarg + 1
 if ~ arg4 eq r9
		mov			r9, arg4
 end if
end if
if ~ arg3 eq
 numarg = numarg + 1
 if ~ arg3 eq r8
		mov			r8, arg3
 end if
end if
if ~ arg2 eq
 numarg = numarg + 1
 if ~ arg2 eq rdx
		mov			rdx, arg2
 end if
end if
if ~ arg1 eq
 numarg = numarg + 1
 if ~ arg1 eq rcx
  if ~ arg1 in <ConsoleInHandle,ConIn,ConsoleOutHandle,ConOut,StandardErrorHandle,StdErr,RuntimeServices,BootServices>
		mov			rcx, arg1
  end if
 end if
end if
		xor			rax, rax
		mov			al, numarg
if interface in <ConsoleInHandle,ConIn,ConsoleOutHandle,ConOut,StandardErrorHandle,StdErr,RuntimeServices,BootServices>
		mov			rbx, [efi_ptr]
		mov			rbx, [rbx + EFI_SYSTEM_TABLE.#interface]
else
 if ~ interface eq rbx
		mov			rbx, interface
 end if
end if
if arg1 in <ConsoleInHandle,ConIn,ConsoleOutHandle,ConOut,StandardErrorHandle,StdErr,RuntimeServices,BootServices>
		mov			rcx, rbx
end if
if defined SIMPLE_INPUT_INTERFACE.#function
		mov			rbx, [rbx + SIMPLE_INPUT_INTERFACE.#function]
else
 if defined SIMPLE_TEXT_OUTPUT_INTERFACE.#function
		mov			rbx, [rbx + SIMPLE_TEXT_OUTPUT_INTERFACE.#function]
 else
  if defined EFI_BOOT_SERVICES_TABLE.#function
		mov			rbx, [rbx + EFI_BOOT_SERVICES_TABLE.#function]
  else
   if defined EFI_RUNTIME_SERVICES_TABLE.#function
		mov			rbx, [rbx + EFI_RUNTIME_SERVICES_TABLE.#function]
   else
    if defined EFI_GRAPHICS_OUTPUT_PROTOCOL.#function
		mov			rbx, [rbx + EFI_GRAPHICS_OUTPUT_PROTOCOL.#function]
    else
     if defined EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE.#function
		mov			rbx, [rbx + EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE.#function]
     else
		mov			rbx, [rbx + function]
     end if
    end if
   end if
  end if
 end if
end if
		call			uefifunc
}
 
section '.text' code executable readable
 
uefifunc:
		mov			qword [uefi_rsptmp], rsp
		and			esp, 0FFFFFFF0h
		bt			eax, 0
		jnc			@f
		push			rax
@@:		cmp			al, 11
		jb			@f
		push			rdi
@@:		cmp			al, 10
		jb			@f
		push			rsi
@@:		cmp			al, 9
		jb			@f
		push			r14
@@:		cmp			al, 8
		jb			@f
		push			r13
@@:		cmp			al, 7
		jb			@f
		push			r12
@@:		cmp			al, 6
		jb			@f
		push			r11
@@:		cmp			al, 5
		jb			@f
		push			r10
@@:
		sub			rsp, 4*8
		call			rbx
		mov			rsp, qword [uefi_rsptmp]
		ret
 
section '.data' data readable writeable
efi_handler:	dq			0
efi_ptr:	dq			0
uefi_rsptmp:	dq			0

листинг 2

пока что пуст, но по мере надобности начнет расти!

if ~ defined __EFICODE__
__EFICODE__ EQU 0

end if

листинг 3
if ~ defined __MACROEFI__
__MACROEFI__ EQU 0


;; --- constants --- ;;
true EQU 1
false EQU 0
null EQU 0
nl EQU 13,10
via EQU ,

;; --- structures --- ;;

struc sString [value] {
	common
	if ~ value eq 
	. du value, null
	else
	. du null
	end if
}

struc sStrbuf _len {
	if ~ len eq 
.len dq _len*2
.val rw _len
	else
.len dq 1024*2
.val rw 1024
	end if
}

struc sKey scan, utf {
	if ~ scan eq 
.scancode dw scan
	else
.scancode dw null
	end if
	if ~ utf eq 
.unicode du utf
	else
.unicode:
	end if
	du null
}

;; --- macros --- ;;

macro mEntry _ofs {
	format pe64 dll efi
	entry _ofs
}

macro mInit {
	InitializeLib
	jnc @f
	mExit EFI_SUCCESS
@@:
}

macro mPrint _str {
    if ~_str eq 
	uefi_call_wrapper ConOut, OutputString, ConOut, _str
	end if
}

macro mPrintln _str {
	if ~ _str eq 
	mPrint _str
	end if
	mPrint _crlf
}

macro mReturn [data] {
	if ~ data eq 
	forward
	push data
	end if
	common
	ret
}

macro mInvoke func, [arg] {
	if ~ arg eq 
	reverse
	push arg
	end if
	call func
}; vacuum example: mInvoke fSend via message

macro mEfidata {
	common
	mSect data
	__crlf sString nl
	__key_buf sKey null, null
}

macro mScankey _key {
	if ~ _key eq 
	mov [_key], dword 0
@@:
	uefi_call_wrapper ConIn, ReadKeyStroke, ConIn, _key
	cmp dword [_key], 0
	jz @b
	end if
}

macro mExit status {
	if status eq 
	mov eax, EFI_SUCCESS
	else
	mov eax, status
	end if
	retn
}

macro mSect name, type {
	if type eq data
	section '.#name' data readable writable
	else if type eq code
	section '.#name' code executable readable
	else if type eq text
	section '.#name' code executable readable
	else if type eq fixup
	section '.#name' fixups data discardable
	end if
}

end if

Традиции... Привет, мир!

Напишем helloworld для uefi используя мои листинги. Код получился до смешного коротким и интуитивно понятным. Имхо, под Windows консольную писалку создать сложнее!

; импортируем макросы
include "include/macroefi.inc"

; назначаем точку входа
mEntry main

; импортируем вторую часть библиотеки (обязательно после mEntry!)
include "include/sysuefi.inc"

; секция '.text' code executable readable
mSect text, code

; главная функция
main:
	  ; инициализируем библеотеку
    mInit
    ; печатаем хеловорлд
    mPrint hello
    ; аналог _getch из msvcrt.dll,
    ; ждем клавиши и сохраняем в key
    mScankey key
    ; возвращаемся в UEFI shell
    ; со статусом ОК
    mExit EFI_SUCCESS

; импортируем третью часть библиотеки (желательно посте основного кода)
include "include/libuefi.inc"

; секция '.bsdata' data readable writable
mSect bsdata, data

; utf-8 строка для хеловорлда
hello sString 'Hello UEFI World!'

; ячейка для клавиши
key sKey

; финальный макрос библиотеки. ВСЕГДА в конце всего кода
mEfidata

Как это собрать?

Я создал для этого простенький Makefile. напишите make build и найдете в рабочей папке файл bootx64.efi. Makefile (подсветка от перл потому, что от нее все подсветилось):

build:
	fasm BOOTX64.asm
image: build
	mkdir tmp
	mkdir tmp/efi
	mkdir tmp/efi/boot
	cp BOOTX64.efi tmp/efi/boot/BOOTX64.efi
	genisoimage -o ./image.iso -V BACKUP -R -J ./tmp
	rm tmp/efi/boot/*
	rmdir tmp/efi/boot tmp/efi tmp
	cls
dump: build
	hd BOOTX64.efi

Как это запустить?

Создаете FAT32 флешку или GPT раздел, кидаете в /efi/boot файл под названием bootx64.efi и перезагружаете ПК. При запуске откройте меню выбора загрузочного носителя и там найдите свой файл/флешку/раздел. Грузитесь с него и видите сообщение. Чтобы вернуться, нажмите любую клавишу.

Итоги

В течении статьи я изложил вам все преимущества Secure boot над Legacy, рассказал как все это работает и написал helloworld-загрузчик.

Поддержка

Всегда непротив конструктивной критики. Принимаю идеи по улучшению статьи и проекта в целом. Всегда буду рад полезным ссылкам. Спасибо за прочтение!

Only registered users can participate in poll. Log in, please.
Да или нет?
54.05% Точно да100
13.51% Да25
11.35% Скорее да, чем нет21
7.03% Не знаю13
8.65% Скорее нет, чем да16
1.62% Нет3
3.78% Точно нет7
185 users voted. 40 users abstained.
Tags:
Hubs:
Total votes 24: ↑22 and ↓2+28
Comments25

Articles