Logo logo o Dummy Kernel (veja o post anterior) vai dar lugar a um Kernel que faça algo de verdade. E quando isso acontecer, o arquivo não vai mais ser escrito em Assembly, mas em C/C++. Conforme o projeto vai ficando mais complexo, o formato binário que estamos usando deixa de ser atraente da forma como está, pois este formato perde os símbolos e outras informações relevantes (lembra que para debugar estamos usando o arquivo objeto .o? Pois é).
O P1 espera que você consiga extrair o “código” dos arquivos ELF do bootloader e do Kernel, juntar os dois e criar uma nova imagem bootável. Obviamente há um caráter pedagógico aqui, você pode aprender muito lendo o belo manual e entender do que se trata cada informação. Faremos isso, mas antes de prosseguir, informo que é possível fazer de uma forma mais fácil: você pode usar o utilitário objcopy, extrair o conteúdo relevante dos arquivos executáveis ELF, juntar os dois e depois sobrescrever os bytes 2, 3, 4 e 5 desse arquivo com o tamanho do kernel (talvez usando awk? É só uma sugestão :D ), considerando a unidade número de setores do disco (considere ainda o byte order, que no caso do x86 é little endian). Se você fizer isso, parabéns, trabalho concluído (mas você não ganharia nota). Até por isso, deixo isso como exercício.
Nós vamos pelo caminho mais difícil: Faremos um programa em C/C++ para abrir os arquivos binários ELF, extrair as informações, colar elas mal e porcamente com cuspe e maisena e gerar uma imagem bootável, tal qual é pregado no P1.
Antes, vamos deixar nosso kernel um pouquinho mais complexo. Vamos adicionar seções .bss
, .data
e .rdata
. Isso nos obrigará a fazer um parser mais elaborado. O final do arquivo kernel.asm fica assim:
...
big_big_msg db "Big Big Dummy Kernel Loaded!", CARRIER_RETURN, LINE_FEED, NULL_CHARACTER
SECTION .bss
bsssegtest resb 50
SECTION .data
datasegtest db 'X'
SECTION .rdata
teste db 'readonlydata'
Depois veremos no que isso resultará.
O primeiro passo é alterar o Makefile
para que ele gere arquivos ELF. Os trechos de geração os targets booloader
e kernel
eram assim:
bootloader: bootloader.o
ld -nostdlib -O2 -g -m elf_i386 -Ttext 0x0 -s --oformat binary -o $(OUTPUTDIR)/$@ $(OUTPUTDIR)/$<
kernel: kernel.o
ld -nostdlib -O2 -g -m elf_i386 -Ttext 0x1000 -s --oformat binary -o $(OUTPUTDIR)/$@ $(OUTPUTDIR)/$<
e ficou assim:
bootloader: bootloader.o
ld -nostdlib -O2 -g -m elf_i386 -Ttext 0x0 -o $(OUTPUTDIR)/$@ $(OUTPUTDIR)/$<
kernel: kernel.o
ld -nostdlib -O2 -g -m elf_i386 -Ttext 0x1000 -o $(OUTPUTDIR)/$@ $(OUTPUTDIR)/$<
Mais simples né? Isso porque o formato ELF é o padrão no Linux. Além disso, removemos a flag -s
pois isso remove os símbolos de debug, o que está longe de ser nosso objetivo.
Aproveitando que estamos no Makefile
, vamos criar o target createimage
, que é um programa que lerá os ELFs e os unirá.
createimage: $(SRCDIR)/createimage.cpp
g++ -o $(OUTPUTDIR)/$@ $<
Aqui, uma diferença em relação ao código fornecido no trabalho P1: lá, esse programa era escrito em C. Até para diferenciar, resolvi fazer diferente.
Pensei em fazer em Rust, mas lembrei que não sei Rust, pareceu pouco produtivo aprender outra linguagem de programação para fazer só isso (NOTA DO EDITOR: Rust será aprendida, eventualmente. Mas não agora e não para isso).
Parti então para o Python, e a manipulação de arquivos binários foi bem frustrante. Possível, mas frustrante. Não vale o esforço. Decidi fazer então com um mix de C e C++ (ou como dizia meu professor, C+/-).
Não abri o código original, o código está estruturado para que os alunos alterem partes específicas e completem as funções:
read_exec_file(): Reads in an executable file in ELF format.
write_bootblock(): Writes the bootblock to the image file.
write_kernel(): Writes the kernel to the image file.
count_kernel_sectors(): Counts the number of sectors in the kernel.
record_kernel_sectors(): Tells the bootloader how many sectors the kernel has.
extended_opt(): Prints the information for --extended option.
Eu não entendo por exemplo porque há a função extended_opt
, e o que é feito de diferente se a função —extended
não for usada. Por mim isso nem seria uma opção mas o comportamento padrão do código. Também não sei quais parâmetros as funções exigem, por isso vou fazer conforme as vozes da minha cabeça. Mas não vou brigar com a banca, tentarei fazer seguindo essas instruções e por isso, em vez de escrever direto na saída padrão (usando cout
do C++), escreverei em uma ostringstream
, servindo como um string buffer, e só ao final do processo mando isso para o console.
Ok, vamos lá. Para abrir o arquivo ELF, entender sua estrutura para poder extrair as informações necessárias, você precisa conhecer as entranhas do cidadão. É importante ler a documentação, mas adianto que não é necessário ler tudo. Uma boa parte do conteúdo diz respeito a como um Sistema Operacional deve “ler” para carregar o executável na memória, incluindo bibliotecas compartilhadas, ligação dinâmica, a biblioteca padrão do C… Crianças, nada disso está disponível quando a máquina liga, não há um SO que carregará tais componentes, afinal, nós somos o SO! Então, precisamos de duas coisas: ler o cabeçalho do arquivo ELF, encontrar os cabeçalhos do programa e com base neles, carregar os segmentos de interesse. Nem todo segmento possui um código executável, então precisamos também identificar isso. Com base no que eu disse, vamos dar uma olhada no conteúdo dos arquivos ELF usando o utilitário readelf
.
Começando pelo Bootloader:
readelf -a ./build/bootloader
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 52 (bytes into file)
Start of section headers: 5132 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 40 (bytes)
Number of section headers: 9
Section header string table index: 8
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 001000 0000a0 00 AX 0 0 16
[ 2] .debug_aranges PROGBITS 00000000 0010a0 000020 00 0 0 1
[ 3] .debug_info PROGBITS 00000000 0010c0 000069 00 0 0 1
[ 4] .debug_abbrev PROGBITS 00000000 001129 00001d 00 0 0 1
[ 5] .debug_line PROGBITS 00000000 001146 00007f 00 0 0 1
[ 6] .symtab SYMTAB 00000000 0011c8 000120 10 7 14 4
[ 7] .strtab STRTAB 00000000 0012e8 0000cc 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 0013b4 000056 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
There are no section groups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x00000000 0x00000000 0x000a0 0x000a0 R E 0x1000
Section to Segment mapping:
Segment Sections...
00 .text
There is no dynamic section in this file.
There are no relocations in this file.
No processor specific unwind information to decode
Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS src/bootloader.asm
2: 00000002 2 OBJECT LOCAL DEFAULT 1 os_size
3: 00000006 1 OBJECT LOCAL DEFAULT 1 disk_index
4: 00000007 1 OBJECT LOCAL DEFAULT 1 sectors_per_track
5: 00000008 1 OBJECT LOCAL DEFAULT 1 number_of_heads
6: 00000009 2 OBJECT LOCAL DEFAULT 1 current_lba_address
7: 0000000b 0 NOTYPE LOCAL DEFAULT 1 load_kernel
8: 0000002f 0 NOTYPE LOCAL DEFAULT 1 before_jump
9: 00000034 0 NOTYPE LOCAL DEFAULT 1 new_boot_region
10: 00000039 0 NOTYPE LOCAL DEFAULT 1 reset_disk
11: 00000041 0 NOTYPE LOCAL DEFAULT 1 get_disk_parameters
12: 00000061 0 NOTYPE LOCAL DEFAULT 1 read_sector
13: 0000009b 0 NOTYPE LOCAL DEFAULT 1 end_read_sector
14: 00000000 0 NOTYPE GLOBAL DEFAULT 1 _start
15: 00001000 0 NOTYPE GLOBAL DEFAULT 1 __bss_start
16: 00001000 0 NOTYPE GLOBAL DEFAULT 1 _edata
17: 00001000 0 NOTYPE GLOBAL DEFAULT 1 _end
No version information found in this file.
Do que nos interessa, é ler esse cabeçalho (ELF Header
). Nele, perceba a informação Start of program headers: 52 (bytes into file)
. Isso significa que, partindo do primeiro byte desse arquivo ELF, se eu iniciar a leitura do offset 52 eu começarei a ler os dados dos cabeçalhos do programa. E quantos bytes eu devo ler para identificar todos os cabeçalhos do programa? Isso está na informação Size of program headers: 32 (bytes)
.
Ok, e como lemos esse cabeçalho ELF? O manual apresenta a estrutura que podemos usar:
Então, você pode copiar essa estrutura ai, OOOOOOU você faz igual eu fiz e uso a estrutura declarada no arquivo elf.h
. No arquivo C/C++ basta declarar:
#include <elf.h>
Declarei portanto a função read_exec_file
que recebe o std::ifstream exec_file
(lembre-se que estou usando C++). Eu declaro uma variável local Elf32_Ehdr ehdr
, reservando portanto o espaço para lermos os dados do cabeçalho do arquivo. Ao lermos a quantidade de bytes equivalente ao tamanho da estrutura (sizeof(Elf32_Ehdr)
) e armazenando na variável ehdr
, temos os dados que precisamos.
Ai basta rebobinar o arquivo (sabe o que é isso jovem?) e, começando do início do arquivo (std::ios_base::beg
), posicionamos a cabeça de leitura imaginária bem no ponto onde começam os cabeçalhos do programa (ehdr.e_phoff
). Essa parte inicial fica portanto assim:
exec_file.read(reinterpret_cast<char *>(&ehdr), sizeof(Elf32_Ehdr));
exec_file.seekg(ehdr.e_phoff, std::ios_base::beg);
Porque usamos o reinterpret_cast
? A função read
de um objeto std::ifstream
espera receber um ponteiro de char. Poderíamos sim usar um cast normal, já que a estrutura Elf32_Ehdr
não é um objeto C/C++ com comportamentos etc. Então, (char *)&ehdr funcionaria
, mas como estamos no maravilho mundo do C++, eu quero que a leitura comece a cuspir bytes na estrutura sem reclamações adicionais. Para mais informações, veja aqui.
Agora que estamos com a bala na agulha para ler os cabeçalhos de programa, como fazemos isso? Vamos voltar à saída do nosso bom e velho readelf
do bootloader
:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x00000000 0x00000000 0x000a0 0x000a0 R E 0x1000
Não parece tão complicado certo? Não tão rápido. Vamos ver os mesmos dados, mas para o kernel
:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x000b4 0x000b4 R 0x1000
LOAD 0x001000 0x00001000 0x00001000 0x60031 0x60031 R E 0x1000
LOAD 0x062000 0x00062000 0x00062000 0x0000c 0x0000c R 0x1000
LOAD 0x06200c 0x0006300c 0x0006300c 0x00001 0x00038 RW 0x1000
Hum, ai começa a complicar. O bootloader
possui 1 cabeçalho de programa, enquanto o kernel
possui 4. Um dos motivos dessa diferença é justamente aquelas seções que havíamos adicionado anteriormente, lembra? As seções .bss
, .data
e .rdata
. Como ainda não apresentamos a saída do readelf
para o kernel
, faremos isso agora.
readelf -a ./build/kernel
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x1000
Start of program headers: 52 (bytes into file)
Start of section headers: 402300 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 4
Size of section headers: 40 (bytes)
Number of section headers: 12
Section header string table index: 11
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00001000 001000 060031 00 AX 0 0 16
[ 2] .rdata PROGBITS 00062000 062000 00000c 00 A 0 0 1
[ 3] .data PROGBITS 0006300c 06200c 000001 00 WA 0 0 4
[ 4] .bss NOBITS 00063010 06200d 000034 00 WA 0 0 4
[ 5] .debug_aranges PROGBITS 00000000 06200d 000020 00 0 0 1
[ 6] .debug_info PROGBITS 00000000 06202d 000065 00 0 0 1
[ 7] .debug_abbrev PROGBITS 00000000 062092 00001d 00 0 0 1
[ 8] .debug_line PROGBITS 00000000 0620af 00008f 00 0 0 1
[ 9] .symtab SYMTAB 00000000 062140 000130 10 10 15 4
[10] .strtab STRTAB 00000000 062270 0000a1 00 0 0 1
[11] .shstrtab STRTAB 00000000 062311 000068 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
There are no section groups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x000b4 0x000b4 R 0x1000
LOAD 0x001000 0x00001000 0x00001000 0x60031 0x60031 R E 0x1000
LOAD 0x062000 0x00062000 0x00062000 0x0000c 0x0000c R 0x1000
LOAD 0x06200c 0x0006300c 0x0006300c 0x00001 0x00038 RW 0x1000
Section to Segment mapping:
Segment Sections...
00
01 .text
02 .rdata
03 .data .bss
There is no dynamic section in this file.
There are no relocations in this file.
No processor specific unwind information to decode
Symbol table '.symtab' contains 19 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS src/kernel.asm
2: 00001002 0 NOTYPE LOCAL DEFAULT 1 print_char
3: 00001016 0 NOTYPE LOCAL DEFAULT 1 print_string
4: 00001020 0 NOTYPE LOCAL DEFAULT 1 loop_print_string
5: 0000102c 0 NOTYPE LOCAL DEFAULT 1 end_print_string
6: 00001031 0 NOTYPE LOCAL DEFAULT 1 kernel_entry
7: 00001046 1 OBJECT LOCAL DEFAULT 1 msg
8: 00031000 0 NOTYPE LOCAL DEFAULT 1 big_dummy_kernel
9: 00031015 1 OBJECT LOCAL DEFAULT 1 big_msg
10: 00061000 0 NOTYPE LOCAL DEFAULT 1 big_big_dummy_kernel
11: 00061012 1 OBJECT LOCAL DEFAULT 1 big_big_msg
12: 00063010 1 OBJECT LOCAL DEFAULT 4 bsssegtest
13: 0006300c 1 OBJECT LOCAL DEFAULT 3 datasegtest
14: 00062000 1 OBJECT LOCAL DEFAULT 2 teste
15: 00001000 0 NOTYPE GLOBAL DEFAULT 1 _start
16: 00063010 0 NOTYPE GLOBAL DEFAULT 4 __bss_start
17: 0006300d 0 NOTYPE GLOBAL DEFAULT 3 _edata
18: 00063044 0 NOTYPE GLOBAL DEFAULT 4 _end
No version information found in this file.
Note que temos informações ligando os cabeçalhos de programa às seções. Veja abaixo:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x000b4 0x000b4 R 0x1000
LOAD 0x001000 0x00001000 0x00001000 0x60031 0x60031 R E 0x1000
LOAD 0x062000 0x00062000 0x00062000 0x0000c 0x0000c R 0x1000
LOAD 0x06200c 0x0006300c 0x0006300c 0x00001 0x00038 RW 0x1000
Section to Segment mapping:
Segment Sections...
00
01 .text
02 .rdata
03 .data .bss
Então, temos 4 cabeçalhos de programa, e 4 seções de segmento. As seções 01
, 02
e 03
são nossas conhecidas. Note também as flags no cabeçalho de programa: a seção 01
.text
tem flag R E
(Leitura e Execução), a seção 02 .rdata
é uma seção somente leitura (dai o nome, rdata
é referente a read only data) e possui somente a flag R, e a seção 03 .data .bss
é auto explicativa, e devemos poder ler escrever dela, e dai também as flags RW
. A única observação interessante é a função .bss, que apenas instrui quantos bytes devem ser reservados na memória para dados não inicializados. Como isso é apenas uma instrução, ele não consome dados no FileSiz
do segmento (esse 0x00001
byte é referente à variavel datasegtest
no segmento .data
) mas instrui que a memória alocada seja feita para isso (MemSiz
igual a 0x00038
contempla a memória tanto para as seções .data
quanto para .bss
).
Mas e quanto ao segmento de seção 00
? Se você notar o offset está 0x000000
e possui MemSiz
/FileSiz
igual a 0xb4
. O que é isso afinal? Bom, se você notar bem, Isso é justamente os dados ocupados pelo ELF Header ( sizeof(Elf32_Ehdr)
= 52 bytes) juntamente com os 4 cabeçalhos de programa, o próprio incluso. Como sizeof(Elf32_Phdr)
= 32 bytes, temos que 52 + 4 * 32 = 180 = 0xb4! Então, a regra é que se o cabeçalho de programa tiver offset igual a zero, ele não é um código que deve ser extraído do arquivo ELF e nesse caso o ignoramos. Por que isso não apareceu no bootloader
, eu não sei dizer.
Como o cabeçalho ELF já possui quantos cabeçalhos de programas há (Number of program headers: 4)
, está bem fácil fazer a leitura desses caras. Basta iterar pelo campo e_phnum
da estrutura Elf32_Ehdr
e ler a estrutura Elf32_Phdr
nos mesmos moldes que fizemos anteriores. No fim, a função read_exec_file
retorna um vetor de Elf32_Phdr
. A função completa fica assim:
std::vector<Elf32_Phdr> read_exec_file(std::ifstream &exec_file) {
Elf32_Ehdr ehdr;
exec_file.read(reinterpret_cast<char *>(&ehdr), sizeof(Elf32_Ehdr));
exec_file.seekg(ehdr.e_phoff, std::ios_base::beg);
std::vector<Elf32_Phdr> program_headers;
for (int i = 0; i < ehdr.e_phnum; i++) {
Elf32_Phdr ph;
exec_file.read(reinterpret_cast<char *>(&ph), sizeof(Elf32_Phdr));
if (ph.p_offset > 0) {
/*offset 0 has ELF Header and Program Headers, it is not a Executable
Code*/
program_headers.insert(program_headers.end(), ph);
}
}
return program_headers;
}
Com o vetor Elf32_Phdr
em mãos, o trabalho é iterar por esse vetor, e para cada cabeçalho, localizar o programa no ELF (campo p_offset
de Elf32_Phdr
) e ler os bytes disponíveis (campo p_filesz
). Jogamos isso em um vetor de bytes (usei o tipo u_int8_t
, mas poderia ser char
também). Se p_memsz
for maior que p_filesz
, então precisamos alocar mais espaço (caso da seção .bss
). Outra informação importante é que os endereços dos cabeçalhos de programa estão ordenados. Se dado um cabeçalho de programa que não seja o último, se seu PhysAddr + MemSiz
for menor que o PhysAddr
do próximo cabeçalho de programa, isso significa que há um espaço de padding que precisa ser incluído entre os conteúdos. Além disso, se for o último cabeçalho de programa e ele tiver menos de 512 bytes, fazemos um padding
para atingir esse valor também.
A função de extrair os segmentos para um vetor de bytes ficou assim:
std::vector<u_int8_t> extract_segments(std::string exec_path,
std::vector<Elf32_Phdr> ph,
std::ifstream &exec_file,
std::ostringstream &extended_string) {
std::vector<u_int8_t> exec_bytes;
extended_string << "0x" << std::hex << std::setw(4) << std::setfill('0')
<< ph[0].p_vaddr << ": " << exec_path << std::endl;
for (int i = 0; i < ph.size(); i++) {
extended_string << "\tsegment " << i << std::endl;
extended_string << "\t\toffset 0x" << std::hex << std::setw(4)
<< std::setfill('0') << ph[i].p_offset << "\t vaddr 0x"
<< std::hex << std::setw(4) << std::setfill('0')
<< ph[i].p_vaddr << std::endl;
extended_string << "\t\tfilesz 0x" << std::hex << std::setw(4)
<< std::setfill('0') << ph[i].p_filesz << "\t memsz 0x"
<< std::hex << std::setw(4) << std::setfill('0')
<< ph[i].p_memsz << std::endl;
exec_file.seekg(ph[i].p_offset, std::ios_base::beg);
uint8_t buff[ph[i].p_filesz];
exec_file.read(reinterpret_cast<char *>(buff), ph[i].p_filesz);
exec_bytes.insert(exec_bytes.end(), buff, buff + ph[i].p_filesz);
if (ph[i].p_filesz < ph[i].p_memsz) {
exec_bytes.insert(exec_bytes.end(), ph[i].p_memsz - ph[i].p_filesz,
uint8_t(0));
}
if (i != ph.size() - 1 &&
ph[i].p_vaddr + ph[i].p_memsz < ph[i + 1].p_vaddr) {
exec_bytes.insert(exec_bytes.end(),
ph[i + 1].p_vaddr - (ph[i].p_vaddr + ph[i].p_memsz),
uint8_t(0));
extended_string << "\t\tpadding up to 0x" << std::hex << std::setw(4)
<< std::setfill('0') << ph[i + 1].p_vaddr << std::endl;
} else if (i == ph.size() - 1 && exec_bytes.size() % SECTOR_SIZE > 0) {
extended_string << "\t\tpadding up to 0x" << std::hex << std::setw(4)
<< std::setfill('0')
<< ph[i].p_vaddr + ph[i].p_memsz +
(SECTOR_SIZE - (exec_bytes.size() % SECTOR_SIZE))
<< std::endl;
exec_bytes.insert(exec_bytes.end(), SECTOR_SIZE - (exec_bytes.size() % SECTOR_SIZE),
uint8_t(0));
}
}
return exec_bytes;
}
Acabamos com dois vetores de bytes, um representando o binário do bootloader
e outro representando o binário do kernel
. Precisamos agora ler quantos setores o binário do kernel
possui e escrever essa informação, na posição correta no bootloader
. Começando então pela contagem dos bytes do kernel
, não há muito segredo. Dividimos o número de bytes do kernel
por 512 (tamanho de um setor) e pegamos a parte inteira da divisão. Se essa divisão tiver resto, significa que precisamos ler um setor a mais para contemplar o restante dos dados. Nada de mais portanto. A função count_kernel_sectors
ficou assim:
uint32_t count_kernel_sectors(std::vector<u_int8_t> &exec_bytes,
std::ostringstream &extended_string) {
uint32_t os_size = exec_bytes.size() % SECTOR_SIZE != 0
? uint32_t(exec_bytes.size() / SECTOR_SIZE) + 1
: uint32_t(exec_bytes.size() / SECTOR_SIZE);
extended_string << "os_size: " << std::dec << os_size << " sectors"
<< std::endl;
return os_size;
}
Note que estamos retornando um uint32_t
, que é um inteiro de 32 bits sem sinal. Precisamos escrever esse número a partir do byte 2 do booloader
(os dois primeiros bytes são a instrução jmp load_kernel)
. A gente faz uma ginástica com os ponteiros, pego o segundo byte do vetor de bytes do bootloader
, pego o endereço desse byte, faço um cast para falar que quero um ponteiro para um uint32_t
nessa posição, e falo que o conteúdo desse endereço tem na verdade o valor os_size
. A função record_kernel_sectors
fica assim:
void record_kernel_sectors(uint32_t os_size,
std::vector<u_int8_t> &exec_bootloader_bytes) {
(*(uint32_t *)&exec_bootloader_bytes[2]) = os_size;
}
A arquitetura é little endian, então o compilador já se encarrega de escrever os bytes menos significativos primeiro. Isso é o que nosso bootloader espera então está tudo certo.
Depois, é só escrever no arquivo de saída boringos.img
, primeiro o vetor de bytes do bootloader
, e depois o vetor de bytes do kernel
. Esse trecho fica assim:
std::ofstream kernelimage("./build/boringos.img");
kernelimage.write(reinterpret_cast<char *>(&bootloader_bytearray[0]),
bootloader_bytearray.size());
kernelimage.write(reinterpret_cast<char *>(&kernel_bytearray[0]),
kernel_bytearray.size());
Ufa, é isso. O código completo está no meu github. Se você rodar um make clean && make
, e rodar o QEMU+gdb,
o resultado será idêntico ao trabalho anterior. O que você pode fazer de diferente é ver o conteúdo da memória dos novos segmentos, em especial o .rdata
e .data
. Faça isso, obviamente, depois que o kernel
já estiver carregado na memória (coloquei um break point em big_big_dummy_kernel
com o comando hb *big_big_dummy_kernel
):
Aparentemente, conseguimos ler o arquivo ELF do kernel certinho, o conteúdo das seções .rdata
e .bss
é esse mesmo apresentado na imagem acima, como era o nosso objetivo!
Com isso fechamos o P1, e vamos para o P2. Esse próximo trabalho espera que construamos um kernel não-preemptivo. Mas já vi aqui que o bootloader do P2 faz mais do que estamos fazendo no nosso código atual, ele entra em modo protegido e então o kernel já pode ser escrito em C/C++ com registradores de 32 bits. Mas tem bastante coisa pra fazer antes disso. O quanto, só botando a mão na massa pra saber…