我正在编写一个包含我的32位内核的Multiboot兼容的ELF可执行文件.我的主要问题是我在生成可执行文件时收到一系列链接器错误:
重定位被截断以适合:R_386_16对``.text'
链接器脚本,代码和构建脚本如下
我决定尝试在我的操作系统中实现VESA VBE图形.我在OSDev论坛中发现了一个现有的VESA驱动程序,我试图将它集成到我自己的操作系统中.我尝试将它添加到我的源目录,用NASM组装并将其链接到LD的最终可执行文件中.我收到的具体错误是:
vesa.asm:(.text+0x64): relocation truncated to fit: R_386_16 against `.text' obj/vesa.o: In function `svga_mode': vesa.asm:(.text+0x9d): relocation truncated to fit: R_386_16 against `.text' vesa.asm:(.text+0xb5): relocation truncated to fit: R_386_16 against `.text' obj/vesa.o: In function `done': vesa.asm:(.text+0xc7): relocation truncated to fit: R_386_16 against `.text'
导致错误的行(按顺序)如下:
mov ax,[vid_mode] mov cx,[vid_mode] mov bx,[vid_mode] jmp 0x8:pm1
我还评论了"链接器错误"的行
这是文件(vesa.asm):
BITS 32 global do_vbe save_idt: dd 0 dw 0 save_esp: dd 0 vid_mode: dw 0 do_vbe: cli mov word [vid_mode],ax mov [save_esp],esp sidt [save_idt] lidt [0x9000] ;; saved on bootup see loader.asm jmp 0x18:pmode pmode: mov ax,0x20 mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov eax,cr0 dec eax mov cr0,eax jmp 0:realmode1 [bits 16] realmode1: xor ax,ax mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov sp,0xf000 sti ;; first zero out the 256 byte memory for the return function from getmodeinfo cld ;; ax is already zero! I just saved myself a few bytes!! mov cx,129 mov di,0x5000 rep stosw mov ax,[vid_mode] ; Linker error xor ax,0x13 jnz svga_mode ;; Ok, just a regular mode13 mov ax,0x13 int 0x10 ;; we didnt actually get a Vidmode structure in 0x5000, so we ;; fake it with the stuff the kernel actually uses mov word [0x5001],0xDD ; mode attribs, and my favorite cup size mov word [0x5013],320 ; width mov word [0x5015],200 ; height mov byte [0x501a],8 ; bpp mov byte [0x501c],1 ; memory model type = CGA mov dword [0x5029],0xa0000 ; screen memory jmp done svga_mode: mov ax,0x4f01 ; Get mode info function mov cx,[vid_mode] ; Linker error or cx,0x4000 ; always try to use linear buffer mov di,0x5001 int 0x10 mov [0x5000],ah or ah,ah jnz done mov ax,0x4f02 ; Now actually set the mode mov bx,[vid_mode] ; ; Linker error or bx,0x4000 int 0x10 done: cli mov eax,cr0 inc eax mov cr0,eax jmp 0x8:pm1 ; Linker error [bits 32] pm1: mov eax,0x10 mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov dword esp,[save_esp] lidt [save_idt] ret
主条目文件(entry.asm):
extern kmain extern do_vbe ; Multiboot Header MBALIGN equ 1<<0 MEMINFO equ 1<<1 ;VIDINFO equ 1<<2 FLAGS equ MBALIGN | MEMINFO; | VIDINFO MAGIC equ 0x1BADB002 CHECKSUM equ -(MAGIC + FLAGS) section .text align 4 dd MAGIC dd FLAGS dd CHECKSUM ;dd 0 ;dd 0 ;dd 0 ;dd 0 ;dd 0 ;dd 0 ;dd 800 ;dd 600 ;dd 32 STACKSIZE equ 0x4000 global entry entry: mov esp, stack+STACKSIZE push eax push ebx call do_vbe cli call kmain cli hlt hang: jmp hang section .bss align 32 stack: resb STACKSIZE
我的链接器脚本:
OUTPUT_FORMAT(elf32-i386) ENTRY(entry) SECTIONS { . = 100000; .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } }
我的构建脚本(注意我使用的是Cygwin):
cd src for i in *.asm do echo Assembling $i nasm -f elf32 -o "../obj/${i%.asm}.o" "$i" done for i in *.cpp do echo Compiling $i i686-elf-g++ -c "$i" -o "../obj/${i%.cpp}.o" -I ../include --freestanding -fno-exceptions -fno-rtti -std=c++14 -Wno-write-strings done for i in *.S do echo Compiling $i i686-elf-as -c "$i" -o "../obj/${i%.S}.o" done for i in *.c do echo Compiling $i i686-elf-gcc -c "$i" -o "../obj/${i%.cpp}.o" -I ../include --freestanding done cd .. i686-elf-ld -m elf_i386 -T linkscript.ld -o bin/kernel.sys obj/*.o
如果它有帮助,这里是目录结构:
/src Source Files /include Include files /obj Object files /bin Kernel Executable
Michael Petc.. 8
This error you received:
relocation truncated to fit: R_386_16 against `.text'
is effectively telling you that when the linker attempted to resolve these relocations within the .text
section that it was unable to do so because the Virtual Memory Addresses (VMA) it computed could not fit in a 16-bit pointer (_16
).
If you use -g -Fdwarf
when assembling with NASM you can produce more usable output from OBJDUMP using a command like i686-elf-objdump -SDr -Mi8086 vesa.o
.
-S
outputs source
-D
is for disassembly,
-r
shows the relocation information.
The following is the output I get (it differs slightly but the ideas presented here still apply):
0000004a: [bits 16] realmode1: ... mov ax,[vid_mode] ; Linker error 63: a1 0a 00 mov ax,ds:0xa 64: R_386_16 .text ... mov cx,[vid_mode] ; Linker error 9a: 8b 0e 0a 00 mov cx,WORD PTR ds:0xa 9c: R_386_16 .text ... mov bx,[vid_mode] ; ; Linker error b2: 8b 1e 0a 00 mov bx,WORD PTR ds:0xa b4: R_386_16 .text ... jmp 0x8:pm1 ; Linker error c5: ea ca 00 08 00 jmp 0x8:0xca c6: R_386_16 .text
为了简洁起见,我删除了没有任何后果的信息,并...
在输出中替换了它.[bits 16]
除非被覆盖,否则有一个指令强制所有内存地址为16位.作为一个例子c6: R_386_16 .text
意味着在偏移量(0xc6)处有一个重定位,它是一个出现在该.text
部分中的16位指针.记住这一点.现在查看链接器脚本:
. = 100000; .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) }
VMA(原点)是0x100000.在这种情况下,这实际上是所有代码和数据的起点.在最终可执行文件中生成的所有地址都将超过0xFFFF,这是可以容纳在16位指针中的最大值.这就是链接器抱怨的原因.
You can override the default address and operand sizes by specifying DWORD before the label name between the brackets [
and ]
. An absolute 32-bit FAR JMP can be encoded by specifying DWORD before the operand. These lines:
mov ax,[vid_mode] mov cx,[vid_mode] mov bx,[vid_mode] jmp 0x8:pm1
Would become:
mov ax,[dword vid_mode] mov cx,[dword vid_mode] mov bx,[dword vid_mode] jmp dword 0x8:pm1
If you assemble the revised code and use OBJDUMP as discussed above you get this output (cut for brevity):
mov ax,[dword vid_mode] ; Linker error 63: 67 a1 0a 00 00 00 addr32 mov ax,ds:0xa 65: R_386_32 .text ... mov cx,[dword vid_mode] ; Linker error 9d: 67 8b 0d 0a 00 00 00 addr32 mov cx,WORD PTR ds:0xa a0: R_386_32 .text ... mov bx,[dword vid_mode] ; ; Linker error b8: 67 8b 1d 0a 00 00 00 addr32 mov bx,WORD PTR ds:0xa bb: R_386_32 .text ... jmp dword 0x8:pm1 ; Linker error ce: 66 ea d6 00 00 00 08 jmp 0x8:0xd6 d5: 00 d0: R_386_32 .text
The instructions now have a 0x66
and 0x67
prefix added to them and the addresses take 4 bytes in the instruction. Each of the relocations are of type R_386_32
which tells the linker that the addresses to relocate are 32-bits wide.
Although the changes in the previous section will eliminate the warnings during linking, when run things may not work as expected (including crashes). On an 80386+ you can generate 16-bit real mode code that uses 32-bit addresses for the data but the CPU has to be put into a mode that allows such access. The mode that allows 32-bit pointers accessed via the DS segment with values above 0xFFFF is called Unreal Mode. OSDev Wiki has some code that could be used as a basis for such support. Assuming the PICs haven't been remapped and are in their initial configuration then the usual way to implement on demand Unreal Mode is to replace the 0x0d Interrupt handler with something that does:
Query PIC1 OCW3 to see if IRQ5 is being serviced or whether there was a General Protection fault. Without PIC remapping #GP fault and IRQ5 point at the same interrupt vector so one has to distinguish between them.
If the IRQ5 ISR is set then call the previously saved interrupt handler (chaining). At this point we are all done.
If IRQ5 ISR is not set then 0x0d interrupt was called because of a general protection fault. Assume the fault was because of an invalid data access.
Switch into protected mode and use a GDT containing a 16-bit data descriptor that has a base of 0 and a limit of 0xffffffff. Set ES and DS using the corresponding selector.
Leave protected mode
Return from interrupt handler.
If PIC1 has been remapped so as to not conflict with the x86 exception handling interrupts (int 0x08 to int 0x0f) then steps 1,2,3 no longer apply. Remapping the PICs to avoid this conflict is common place in x86 OS design. The code in the question doesn't do any PIC remapping.
This mechanism won't work if you want to ever use the code in a VM8086 task rather than entering real mode.
DOS's HIMEM.SYS did something similar in the 1980s and you can find a discussion about that in this article if you are interested.
Note : Although I give a general description of using Unreal Mode, I don't recommend this method. It requires more extensive knowledge of real mode, protected mode, interrupt handling.
Rather than using 32-bit data pointers greater than 0xFFFF, and ensuring that the processor is in unreal mode there is a solution that may be easier to understand. One such solution is to copy the real mode code and data from where the Multiboot loader physically loaded into RAM above 0x100000 to the first 64KB of memory memory just above the real mode interrupt vector table (IVT). This allows you to continue using 16-bit pointers because the first 64KB of memory is addressable with a 16-bit pointer (0x0000 to 0xFFFF). The 32-bit code will still be able to access the real mode data if needed.
To do this you will have to create a more complex GNU LD linker script (link.ld
) that uses a Virtual Memory Address (origin point) in lower memory. Address 0x01000 is a good choice. The Multiboot header will still have to be present at the beginning of the ELF executable.
One problem that has to be overcome is that the Multiboot loader will read the code and data into memory above 0x100000. One has to manually copy the 16-bit real mode code and data to address 0x01000 before the real mode code can be used. The linker script can help generate symbols to compute the start and end addresses for such a copy.
See the code in the last section for a linker script link.ld
that does just that, and a kernel.c
file that does the copy.
With properly adjusted VESA code what you are trying to do should work.
The VESA code relies on hard coded addresses
Was not designed with Multiboot in mind as it was assumed the kernel would be manually loaded into memory (<64KB) at a specific place and that certain addresses already contained specific data before being called.
The code doesn't follow the CDECL calling convention thus it isn't callable from C code directly.
There is a bug that placed 32-bit code under a [bits 16]
directive.
The code doesn't show the GDT table that was needed but it can be inferred from the code that there needed to be at least 5 descriptors in a GDT in a specific order.
Null Descriptor
32-bit Code Segment (Base 0, Limit 0xffffffff). Selector 0x08
32-bit Data Segment (Base 0, Limit 0xffffffff). Selector 0x10
16-bit Code Segment (Base 0, Limit at least 0xffff). Selector 0x18
16-bit Data Segment (Base 0, Limit at least 0xffff). Selector 0x20
The author had these comments:
On boot up save the real mode IDT at a known place (mine was at 0x9000) using the sidt instruction, and dont overwrite address 0-0x500 in memory. It also assumes that you are using 8 and 16 as segment registers for code and data in PMode. It stores the result of function 4f01 at 0x5000, and automatically sets bit 13 (use framebuffer)
The following code is a complete implementation of what has been suggested above. Use a linker script and generate real mode code and data and place it starting at 0x1000. The code uses C to set up a proper GDT with 32 and 16-bit Code and Data segments, copies the real mode code from above 0x100000 down to 0x1000. It also fixes the other issues previously identified in the VESA driver code. To test it switches in to video mode 0x13 (320x200x256) and paints part of the VGA color palette to the display 32 bits at a time.
link.ld
:
OUTPUT_FORMAT("elf32-i386"); ENTRY(mbentry); /* Multiboot spec uses 0x00100000 as a base */ PHYS_BASE = 0x00100000; REAL_BASE = 0x00001000; SECTIONS { . = PHYS_BASE; /* Place the multiboot record first */ .multiboot : { *(.multiboot); } /* This is the tricky part. The LMA (load memory address) is the * memory location the code/data is read into memory by the * multiboot loader. The LMA is after the colon. We want to tell * the linker that the code/data in this section was loaded into * RAM in the memory area above 0x100000. On the other hand the * VMA (virtual memory address) specified before the colon acts * like an ORG directive. The VMA tells the linker to resolve all * subsequent code starting relative to the specified VMA. The * VMA in this case is REAL_BASE which we defined as 0x1000. * 0x1000 is 4KB page aligned (useful if you ever use paging) and * resides above the end of the interrupt table and the * BIOS Data Area (BDA) */ __physreal_diff = . - REAL_BASE; .realmode REAL_BASE : AT(ADDR(.realmode) + __physreal_diff) { /* The __realmode* values can be used by code to copy * the code/data from where it was placed in RAM by the * multiboot loader into lower memory at REAL_BASE * * . (period) is the current VMA */ __realmode_vma_start = .; /* LOADADDR is the LMA of the specified section */ __realmode_lma_start = LOADADDR(.realmode); *(.text.realmode); *(.data.realmode); } . = ALIGN(4); __realmode_vma_end = .; __realmode_secsize = ((__realmode_vma_end)-(__realmode_vma_start)); __realmode_secsize_l = __realmode_secsize>>2; __realmode_lma_end = __realmode_vma_start + __physreal_diff + __realmode_secsize; /* . (period) is the current VMA. We set it to the value that would * have been generated had we not changed the VMA in the previous * section. The .text section also specified the LMA = VMA with * AT(ADDR(.text)) */ . += __physreal_diff; .text ALIGN(4K): AT(ADDR(.text)) { *(.text); } /* From this point the linker script is typical */ .data ALIGN(4K) : { *(.data); } .data ALIGN(4K) : { *(.rodata); } /* We want to avoid this section being placed in low memory */ .eh_frame : { *(.eh_frame*); } .bss ALIGN(4K): { *(COMMON); *(.bss) } /* The .note.gnu.build-id section will usually be placed at the beginning * of the ELF object. We discard it (if it is present) so that the * multiboot header is placed as early as possible in the file. The * multiboot header must appear in the first 8K and be on a 4 byte * aligned offset per the multiboot spec. */ /DISCARD/ : { *(.note.gnu.build-id); *(.comment); } }
gdt.inc
:
CODE32SEL equ 0x08 DATA32SEL equ 0x10 CODE16SEL equ 0x18 DATA16SEL equ 0x20
vesadrv.asm
:
; Video driver code - switches the CPU back into real mode ; Then executes an int 0x10 instruction %include "gdt.inc" global do_vbe bits 16 section .data.realmode save_idt: dw 0 dd 0 save_esp: dd 0 vid_mode: dw 0 real_ivt: dw (256 * 4) - 1 ; Realmode IVT has 256 CS:IP pairs dd 0 ; Realmode IVT physical address at address 0x00000 align 4 mode_info:TIMES 129 dw 0 ; Buffer to store mode info from Int 10h/ax=4f01h ; Plus additional bytes for the return status byte ; at beginning of buffer bits 32 section .text do_vbe: mov ax, [esp+4] ; Retrieve videomode passed on stack pushad ; Save all the registers pushfd ; Save the flags (including Interrupt flag) cli mov word [vid_mode],ax mov [save_esp],esp sidt [save_idt] lidt [real_ivt] ; We use a real ivt that points to the ; physical address 0x00000 at the bottom of ; memory. The IVT in real mode is 256*4 bytes ; and runs from physical address 0x00000 to ; 0x00400 jmp CODE16SEL:pmode16 bits 16 section .text.realmode pmode16: mov ax,DATA16SEL mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov eax,cr0 dec eax mov cr0,eax jmp 0:realmode1 realmode1: ; Sets real mode stack to grow down from 0x1000:0xFFFF mov ax,0x1000 mov ss,ax xor sp,sp xor ax,ax mov ds,ax mov es,ax mov fs,ax mov gs,ax ; first zero out the 258 byte memory for the return function from getmodeinfo cld mov cx,(258/2) ; 128 words + 1 word for the status return byte mov di,mode_info rep stosw mov ax,[vid_mode] xor ax,0x13 jnz svga_mode ; Just a regular mode13 mov ax,0x13 int 0x10 ; Fake a video mode structure with the stuff the kernel actually uses mov di, mode_info mov word [di+0x01],0xDD ; mode attribs mov word [di+0x13],320 ; width mov word [di+0x15],200 ; height mov byte [di+0x1a],8 ; bpp mov byte [di+0x1c],1 ; memory model type = CGA mov dword [di+0x29],0xa0000 ; screen memory jmp done svga_mode: mov ax,0x4f01 ; Get mode info function mov cx,[vid_mode] or cx,0x4000 ; always try to use linear buffer mov di,mode_info+0x01 int 0x10 mov [mode_info],ah or ah,ah jnz done mov ax,0x4f02 ; Now actually set the mode mov bx,[vid_mode] or bx,0x4000 int 0x10 done: cli mov eax,cr0 inc eax mov cr0,eax jmp dword CODE32SEL:pm1 ; To FAR JMP to address > 0xFFFF we need ; to specify DWORD to allow a 32-bit address ; in the offset portion. When this JMP is ; complete CS will be CODE32SEL and processor ; will be in 32-bit protected mode bits 32 section .text pm1: mov eax,DATA32SEL mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov dword esp,[save_esp] lidt [save_idt] popfd ; Restore flags (including Interrupt flag) popad ; Restore registers mov eax, mode_info ; Return pointer to mode_info structure ret
vesadrv.h
:
#ifndef VESADRV_H #define VESADRV_H #includeextern struct mode_info_t * do_vbe (const uint8_t video_mode); struct mode_info_t { uint8_t status; /* Return value from Int 10/ax=4f01 */ /* Rest of structure from OSDev Wiki http://wiki.osdev.org/VESA_Video_Modes#VESA_Functions */ uint16_t attributes; uint8_t winA,winB; uint16_t granularity; uint16_t winsize; uint16_t segmentA, segmentB; /* Real mode FAR Pointer. Physical address * computed as (segment<<4)+offset */ uint16_t realFctPtr_offset; /* FAR Pointer offset */ uint16_t realFctPtr_segment;/* FAR Pointer segment */ uint16_t pitch; /* bytes per scanline */ uint16_t Xres, Yres; uint8_t Wchar, Ychar, planes, bpp, banks; uint8_t memory_model, bank_size, image_pages; uint8_t reserved0; uint8_t red_mask, red_position; uint8_t green_mask, green_position; uint8_t blue_mask, blue_position; uint8_t rsv_mask, rsv_position; uint8_t directcolor_attributes; volatile void * physbase; /* LFB (Linear Framebuffer) address */ uint32_t reserved1; uint16_t reserved2; } __attribute__((packed)); #endif
gdt.h
:
#ifndef GDT_H #define GDT_H #include#include typedef struct { unsigned short limit_low; unsigned short base_low; unsigned char base_middle; unsigned char access; unsigned char flags; unsigned char base_high; } __attribute__((packed)) gdt_desc_t; typedef struct { uint16_t limit; gdt_desc_t *gdt; } __attribute__((packed)) gdtr_t; extern void gdt_set_gate(gdt_desc_t gdt[], const int num, const uint32_t base, const uint32_t limit, const uint8_t access, const uint8_t flags); static inline void gdt_load(gdtr_t * const gdtr, const uint16_t codesel, const uint16_t datasel, const bool flush) { /* Dummy variable used as a fake dependency to avoid optimization * reordering of the inline assembly. The flush (if requested) must * always come after we set the GDT, not before */ int dummy; /* load the GDT register */ __asm__ __volatile__ ("lgdt %[gdtr]" : "=X"(dummy) : [gdtr]"m"(*gdtr), /* Dummy constraint to ensure what gdtr->gdt points at is fully * realized into memory before we issue LGDT instruction */ "m"(*(const gdt_desc_t (*)[]) gdtr->gdt)); /* This flushes the selector registers to ensure the new * descriptors are used. */ if (flush) { /* The indirect absolute jump is because we can't * assume that codesel is an immediate value * as it may be passed in a register. We build a * far pointer in memory and indirectly jump through * that pointer. This explicitly sets CS selector */ __asm__ __volatile__ ( "pushl %[codesel]\n\t" "pushl $1f\n\t" "ljmpl *(%%esp)\n" "1:\n\t" "add $8, %%esp\n\t" "mov %[datasel], %%ds\n\t" "mov %[datasel], %%es\n\t" "mov %[datasel], %%ss\n\t" "mov %[datasel], %%fs\n\t" "mov %[datasel], %%gs" : /* No outputs */ : "X"(dummy), [datasel]"r"(datasel), [codesel]"g"((uint32_t)codesel)); } return; } #endif
gdt.c
:
#include "gdt.h" /* Setup a descriptor in the Global Descriptor Table */ void gdt_set_gate(gdt_desc_t gdt[], const int num, const uint32_t base, const uint32_t limit, const uint8_t access, const uint8_t flags) { /* Setup the descriptor base access */ gdt[num].base_low = (base & 0xFFFF); gdt[num].base_middle = (base >> 16) & 0xFF; gdt[num].base_high = (base >> 24) & 0xFF; /* Setup the descriptor limits */ gdt[num].limit_low = (limit & 0xFFFF); gdt[num].flags = ((limit >> 16) & 0x0F); /* Finally, set up the flags and access byte */ gdt[num].flags |= (flags << 4); gdt[num].access = access; }
multiboot.asm
:
%include "gdt.inc" STACKSIZE equ 0x4000 bits 32 global mbentry extern kmain ; Multiboot Header section .multiboot MBALIGN equ 1<<0 MEMINFO equ 1<<1 VIDINFO equ 0<<2 FLAGS equ MBALIGN | MEMINFO | VIDINFO MAGIC equ 0x1BADB002 CHECKSUM equ -(MAGIC + FLAGS) mb_hdr: dd MAGIC dd FLAGS dd CHECKSUM section .text mbentry: cli cld mov esp, stack_top ; EAX = magic number. Should be 0x2badb002 ; EBX = pointer to multiboot_info ; Pass as parameters right to left push eax push ebx call kmain ; Infinite loop to end program cli endloop: hlt jmp endloop section .bss align 32 stack: resb STACKSIZE stack_top:
kernel.c
:
#include#include #include "vesadrv.h" #include "gdt.h" #define CODE32SEL 0x08 #define DATA32SEL 0x10 #define CODE16SEL 0x18 #define DATA16SEL 0x20 #define NUM_GDT_ENTRIES 5 /* You can get this structure from GRUB's multiboot.h if needed * https://www.gnu.org/software/grub/manual/multiboot/html_node/multiboot_002eh.html */ struct multiboot_info; /* Values made available by the linker script */ extern void *__realmode_lma_start; extern void *__realmode_lma_end; extern void *__realmode_vma_start; /* Pointer to graphics memory.Mark as volatile since * video memory is memory mapped IO. Certain optimization * should not be performed. */ volatile uint32_t * video_gfx_ptr; /* GDT descriptor table */ gdt_desc_t gdt[NUM_GDT_ENTRIES]; /* Copy the code and data in the realmode section down into the lower * 64kb of memory @ 0x00001000. */ static void realmode_setup (void) { /* Each of these __realmode* values is generated by the linker script */ uint32_t *src_addr = (uint32_t *)&__realmode_lma_start; uint32_t *dst_addr = (uint32_t *)&__realmode_vma_start; uint32_t *src_end = (uint32_t *)&__realmode_lma_end; /* Copy a DWORD at a time from source to destination */ while (src_addr < src_end) *dst_addr++ = *src_addr++; } void gdt_setup (gdt_desc_t gdt[], const int numdesc) { gdtr_t gdtr = { sizeof(gdt_desc_t)*numdesc-1, gdt }; /* Null descriptor */ gdt_set_gate(gdt, 0, 0x00000000, 0x00000000, 0x00, 0x0); /* 32-bit Code descriptor, flat 4gb */ gdt_set_gate(gdt, 1, 0x00000000, 0xffffffff, 0x9A, 0xC); /* 32-bit Data descriptor, flat 4gb */ gdt_set_gate(gdt, 2, 0x00000000, 0xffffffff, 0x92, 0xC); /* 16-bit Code descriptor, limit 0xffff bytes */ gdt_set_gate(gdt, 3, 0x00000000, 0x0000ffff, 0x9A, 0x0); /* 16-bit Data descriptor, limit 0xffffffff bytes */ gdt_set_gate(gdt, 4, 0x00000000, 0xffffffff, 0x92, 0x8); /* Load global decriptor table, and flush the selectors */ gdt_load(&gdtr, CODE32SEL, DATA32SEL, true); } int kmain(struct multiboot_info *mb_info, const uint32_t magicnum) { struct mode_info_t *pMI; uint32_t pixel_colors = 0; /* Quiet compiler about unused variables */ (void) mb_info; (void) magicnum; /* Setup the GDT */ gdt_setup(gdt, NUM_GDT_ENTRIES); /* Setup real mode code and data */ realmode_setup(); /* Switch to video mode 0x13 (320x200x256) * The physical address of the mode 13 video memory is * 0xa0000 */ pMI = do_vbe(0x13); video_gfx_ptr = pMI->physbase; /* Display part of the VGA palette as a test pattern */ for (int pixelpos = 0; pixelpos < (320*200); pixelpos++) { if ((pixel_colors & 0xff) == (320/4)) pixel_colors = 0; pixel_colors += 0x01010101; video_gfx_ptr[pixelpos] = pixel_colors; } return 0; }
A simple set of commands to assemble/compile/link the code above into an ELF executable called multiboot.elf
:
nasm -f elf32 -g -F dwarf -o multiboot.o multiboot.asm nasm -f elf32 -g -F dwarf -o vesadrv.o vesadrv.asm i686-elf-gcc -std=c99 -g -m32 -O3 -c -fno-exceptions -nostdlib -ffreestanding -Wall -Wextra -o kernel.o kernel.c i686-elf-gcc -std=c99 -g -m32 -O3 -c -fno-exceptions -nostdlib -ffreestanding -Wall -Wextra -pedantic -o gdt.o gdt.c -lgcc i686-elf-gcc -m32 -Tlink.ld -ffreestanding -nostdlib -o multiboot.elf multiboot.o kernel.o gdt.o vesadrv.o -lgcc
You can find a copy of the code above on my site. When I run the kernel in QEMU this is what I see:
This error you received:
relocation truncated to fit: R_386_16 against `.text'
is effectively telling you that when the linker attempted to resolve these relocations within the .text
section that it was unable to do so because the Virtual Memory Addresses (VMA) it computed could not fit in a 16-bit pointer (_16
).
If you use -g -Fdwarf
when assembling with NASM you can produce more usable output from OBJDUMP using a command like i686-elf-objdump -SDr -Mi8086 vesa.o
.
-S
outputs source
-D
is for disassembly,
-r
shows the relocation information.
The following is the output I get (it differs slightly but the ideas presented here still apply):
0000004a: [bits 16] realmode1: ... mov ax,[vid_mode] ; Linker error 63: a1 0a 00 mov ax,ds:0xa 64: R_386_16 .text ... mov cx,[vid_mode] ; Linker error 9a: 8b 0e 0a 00 mov cx,WORD PTR ds:0xa 9c: R_386_16 .text ... mov bx,[vid_mode] ; ; Linker error b2: 8b 1e 0a 00 mov bx,WORD PTR ds:0xa b4: R_386_16 .text ... jmp 0x8:pm1 ; Linker error c5: ea ca 00 08 00 jmp 0x8:0xca c6: R_386_16 .text
为了简洁起见,我删除了没有任何后果的信息,并...
在输出中替换了它.[bits 16]
除非被覆盖,否则有一个指令强制所有内存地址为16位.作为一个例子c6: R_386_16 .text
意味着在偏移量(0xc6)处有一个重定位,它是一个出现在该.text
部分中的16位指针.记住这一点.现在查看链接器脚本:
. = 100000; .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) }
VMA(原点)是0x100000.在这种情况下,这实际上是所有代码和数据的起点.在最终可执行文件中生成的所有地址都将超过0xFFFF,这是可以容纳在16位指针中的最大值.这就是链接器抱怨的原因.
You can override the default address and operand sizes by specifying DWORD before the label name between the brackets [
and ]
. An absolute 32-bit FAR JMP can be encoded by specifying DWORD before the operand. These lines:
mov ax,[vid_mode] mov cx,[vid_mode] mov bx,[vid_mode] jmp 0x8:pm1
Would become:
mov ax,[dword vid_mode] mov cx,[dword vid_mode] mov bx,[dword vid_mode] jmp dword 0x8:pm1
If you assemble the revised code and use OBJDUMP as discussed above you get this output (cut for brevity):
mov ax,[dword vid_mode] ; Linker error 63: 67 a1 0a 00 00 00 addr32 mov ax,ds:0xa 65: R_386_32 .text ... mov cx,[dword vid_mode] ; Linker error 9d: 67 8b 0d 0a 00 00 00 addr32 mov cx,WORD PTR ds:0xa a0: R_386_32 .text ... mov bx,[dword vid_mode] ; ; Linker error b8: 67 8b 1d 0a 00 00 00 addr32 mov bx,WORD PTR ds:0xa bb: R_386_32 .text ... jmp dword 0x8:pm1 ; Linker error ce: 66 ea d6 00 00 00 08 jmp 0x8:0xd6 d5: 00 d0: R_386_32 .text
The instructions now have a 0x66
and 0x67
prefix added to them and the addresses take 4 bytes in the instruction. Each of the relocations are of type R_386_32
which tells the linker that the addresses to relocate are 32-bits wide.
Although the changes in the previous section will eliminate the warnings during linking, when run things may not work as expected (including crashes). On an 80386+ you can generate 16-bit real mode code that uses 32-bit addresses for the data but the CPU has to be put into a mode that allows such access. The mode that allows 32-bit pointers accessed via the DS segment with values above 0xFFFF is called Unreal Mode. OSDev Wiki has some code that could be used as a basis for such support. Assuming the PICs haven't been remapped and are in their initial configuration then the usual way to implement on demand Unreal Mode is to replace the 0x0d Interrupt handler with something that does:
Query PIC1 OCW3 to see if IRQ5 is being serviced or whether there was a General Protection fault. Without PIC remapping #GP fault and IRQ5 point at the same interrupt vector so one has to distinguish between them.
If the IRQ5 ISR is set then call the previously saved interrupt handler (chaining). At this point we are all done.
If IRQ5 ISR is not set then 0x0d interrupt was called because of a general protection fault. Assume the fault was because of an invalid data access.
Switch into protected mode and use a GDT containing a 16-bit data descriptor that has a base of 0 and a limit of 0xffffffff. Set ES and DS using the corresponding selector.
Leave protected mode
Return from interrupt handler.
If PIC1 has been remapped so as to not conflict with the x86 exception handling interrupts (int 0x08 to int 0x0f) then steps 1,2,3 no longer apply. Remapping the PICs to avoid this conflict is common place in x86 OS design. The code in the question doesn't do any PIC remapping.
This mechanism won't work if you want to ever use the code in a VM8086 task rather than entering real mode.
DOS's HIMEM.SYS did something similar in the 1980s and you can find a discussion about that in this article if you are interested.
Note : Although I give a general description of using Unreal Mode, I don't recommend this method. It requires more extensive knowledge of real mode, protected mode, interrupt handling.
Rather than using 32-bit data pointers greater than 0xFFFF, and ensuring that the processor is in unreal mode there is a solution that may be easier to understand. One such solution is to copy the real mode code and data from where the Multiboot loader physically loaded into RAM above 0x100000 to the first 64KB of memory memory just above the real mode interrupt vector table (IVT). This allows you to continue using 16-bit pointers because the first 64KB of memory is addressable with a 16-bit pointer (0x0000 to 0xFFFF). The 32-bit code will still be able to access the real mode data if needed.
To do this you will have to create a more complex GNU LD linker script (link.ld
) that uses a Virtual Memory Address (origin point) in lower memory. Address 0x01000 is a good choice. The Multiboot header will still have to be present at the beginning of the ELF executable.
One problem that has to be overcome is that the Multiboot loader will read the code and data into memory above 0x100000. One has to manually copy the 16-bit real mode code and data to address 0x01000 before the real mode code can be used. The linker script can help generate symbols to compute the start and end addresses for such a copy.
See the code in the last section for a linker script link.ld
that does just that, and a kernel.c
file that does the copy.
With properly adjusted VESA code what you are trying to do should work.
The VESA code relies on hard coded addresses
Was not designed with Multiboot in mind as it was assumed the kernel would be manually loaded into memory (<64KB) at a specific place and that certain addresses already contained specific data before being called.
The code doesn't follow the CDECL calling convention thus it isn't callable from C code directly.
There is a bug that placed 32-bit code under a [bits 16]
directive.
The code doesn't show the GDT table that was needed but it can be inferred from the code that there needed to be at least 5 descriptors in a GDT in a specific order.
Null Descriptor
32-bit Code Segment (Base 0, Limit 0xffffffff). Selector 0x08
32-bit Data Segment (Base 0, Limit 0xffffffff). Selector 0x10
16-bit Code Segment (Base 0, Limit at least 0xffff). Selector 0x18
16-bit Data Segment (Base 0, Limit at least 0xffff). Selector 0x20
The author had these comments:
On boot up save the real mode IDT at a known place (mine was at 0x9000) using the sidt instruction, and dont overwrite address 0-0x500 in memory. It also assumes that you are using 8 and 16 as segment registers for code and data in PMode. It stores the result of function 4f01 at 0x5000, and automatically sets bit 13 (use framebuffer)
The following code is a complete implementation of what has been suggested above. Use a linker script and generate real mode code and data and place it starting at 0x1000. The code uses C to set up a proper GDT with 32 and 16-bit Code and Data segments, copies the real mode code from above 0x100000 down to 0x1000. It also fixes the other issues previously identified in the VESA driver code. To test it switches in to video mode 0x13 (320x200x256) and paints part of the VGA color palette to the display 32 bits at a time.
link.ld
:
OUTPUT_FORMAT("elf32-i386"); ENTRY(mbentry); /* Multiboot spec uses 0x00100000 as a base */ PHYS_BASE = 0x00100000; REAL_BASE = 0x00001000; SECTIONS { . = PHYS_BASE; /* Place the multiboot record first */ .multiboot : { *(.multiboot); } /* This is the tricky part. The LMA (load memory address) is the * memory location the code/data is read into memory by the * multiboot loader. The LMA is after the colon. We want to tell * the linker that the code/data in this section was loaded into * RAM in the memory area above 0x100000. On the other hand the * VMA (virtual memory address) specified before the colon acts * like an ORG directive. The VMA tells the linker to resolve all * subsequent code starting relative to the specified VMA. The * VMA in this case is REAL_BASE which we defined as 0x1000. * 0x1000 is 4KB page aligned (useful if you ever use paging) and * resides above the end of the interrupt table and the * BIOS Data Area (BDA) */ __physreal_diff = . - REAL_BASE; .realmode REAL_BASE : AT(ADDR(.realmode) + __physreal_diff) { /* The __realmode* values can be used by code to copy * the code/data from where it was placed in RAM by the * multiboot loader into lower memory at REAL_BASE * * . (period) is the current VMA */ __realmode_vma_start = .; /* LOADADDR is the LMA of the specified section */ __realmode_lma_start = LOADADDR(.realmode); *(.text.realmode); *(.data.realmode); } . = ALIGN(4); __realmode_vma_end = .; __realmode_secsize = ((__realmode_vma_end)-(__realmode_vma_start)); __realmode_secsize_l = __realmode_secsize>>2; __realmode_lma_end = __realmode_vma_start + __physreal_diff + __realmode_secsize; /* . (period) is the current VMA. We set it to the value that would * have been generated had we not changed the VMA in the previous * section. The .text section also specified the LMA = VMA with * AT(ADDR(.text)) */ . += __physreal_diff; .text ALIGN(4K): AT(ADDR(.text)) { *(.text); } /* From this point the linker script is typical */ .data ALIGN(4K) : { *(.data); } .data ALIGN(4K) : { *(.rodata); } /* We want to avoid this section being placed in low memory */ .eh_frame : { *(.eh_frame*); } .bss ALIGN(4K): { *(COMMON); *(.bss) } /* The .note.gnu.build-id section will usually be placed at the beginning * of the ELF object. We discard it (if it is present) so that the * multiboot header is placed as early as possible in the file. The * multiboot header must appear in the first 8K and be on a 4 byte * aligned offset per the multiboot spec. */ /DISCARD/ : { *(.note.gnu.build-id); *(.comment); } }
gdt.inc
:
CODE32SEL equ 0x08 DATA32SEL equ 0x10 CODE16SEL equ 0x18 DATA16SEL equ 0x20
vesadrv.asm
:
; Video driver code - switches the CPU back into real mode ; Then executes an int 0x10 instruction %include "gdt.inc" global do_vbe bits 16 section .data.realmode save_idt: dw 0 dd 0 save_esp: dd 0 vid_mode: dw 0 real_ivt: dw (256 * 4) - 1 ; Realmode IVT has 256 CS:IP pairs dd 0 ; Realmode IVT physical address at address 0x00000 align 4 mode_info:TIMES 129 dw 0 ; Buffer to store mode info from Int 10h/ax=4f01h ; Plus additional bytes for the return status byte ; at beginning of buffer bits 32 section .text do_vbe: mov ax, [esp+4] ; Retrieve videomode passed on stack pushad ; Save all the registers pushfd ; Save the flags (including Interrupt flag) cli mov word [vid_mode],ax mov [save_esp],esp sidt [save_idt] lidt [real_ivt] ; We use a real ivt that points to the ; physical address 0x00000 at the bottom of ; memory. The IVT in real mode is 256*4 bytes ; and runs from physical address 0x00000 to ; 0x00400 jmp CODE16SEL:pmode16 bits 16 section .text.realmode pmode16: mov ax,DATA16SEL mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov eax,cr0 dec eax mov cr0,eax jmp 0:realmode1 realmode1: ; Sets real mode stack to grow down from 0x1000:0xFFFF mov ax,0x1000 mov ss,ax xor sp,sp xor ax,ax mov ds,ax mov es,ax mov fs,ax mov gs,ax ; first zero out the 258 byte memory for the return function from getmodeinfo cld mov cx,(258/2) ; 128 words + 1 word for the status return byte mov di,mode_info rep stosw mov ax,[vid_mode] xor ax,0x13 jnz svga_mode ; Just a regular mode13 mov ax,0x13 int 0x10 ; Fake a video mode structure with the stuff the kernel actually uses mov di, mode_info mov word [di+0x01],0xDD ; mode attribs mov word [di+0x13],320 ; width mov word [di+0x15],200 ; height mov byte [di+0x1a],8 ; bpp mov byte [di+0x1c],1 ; memory model type = CGA mov dword [di+0x29],0xa0000 ; screen memory jmp done svga_mode: mov ax,0x4f01 ; Get mode info function mov cx,[vid_mode] or cx,0x4000 ; always try to use linear buffer mov di,mode_info+0x01 int 0x10 mov [mode_info],ah or ah,ah jnz done mov ax,0x4f02 ; Now actually set the mode mov bx,[vid_mode] or bx,0x4000 int 0x10 done: cli mov eax,cr0 inc eax mov cr0,eax jmp dword CODE32SEL:pm1 ; To FAR JMP to address > 0xFFFF we need ; to specify DWORD to allow a 32-bit address ; in the offset portion. When this JMP is ; complete CS will be CODE32SEL and processor ; will be in 32-bit protected mode bits 32 section .text pm1: mov eax,DATA32SEL mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov dword esp,[save_esp] lidt [save_idt] popfd ; Restore flags (including Interrupt flag) popad ; Restore registers mov eax, mode_info ; Return pointer to mode_info structure ret
vesadrv.h
:
#ifndef VESADRV_H #define VESADRV_H #includeextern struct mode_info_t * do_vbe (const uint8_t video_mode); struct mode_info_t { uint8_t status; /* Return value from Int 10/ax=4f01 */ /* Rest of structure from OSDev Wiki http://wiki.osdev.org/VESA_Video_Modes#VESA_Functions */ uint16_t attributes; uint8_t winA,winB; uint16_t granularity; uint16_t winsize; uint16_t segmentA, segmentB; /* Real mode FAR Pointer. Physical address * computed as (segment<<4)+offset */ uint16_t realFctPtr_offset; /* FAR Pointer offset */ uint16_t realFctPtr_segment;/* FAR Pointer segment */ uint16_t pitch; /* bytes per scanline */ uint16_t Xres, Yres; uint8_t Wchar, Ychar, planes, bpp, banks; uint8_t memory_model, bank_size, image_pages; uint8_t reserved0; uint8_t red_mask, red_position; uint8_t green_mask, green_position; uint8_t blue_mask, blue_position; uint8_t rsv_mask, rsv_position; uint8_t directcolor_attributes; volatile void * physbase; /* LFB (Linear Framebuffer) address */ uint32_t reserved1; uint16_t reserved2; } __attribute__((packed)); #endif
gdt.h
:
#ifndef GDT_H #define GDT_H #include#include typedef struct { unsigned short limit_low; unsigned short base_low; unsigned char base_middle; unsigned char access; unsigned char flags; unsigned char base_high; } __attribute__((packed)) gdt_desc_t; typedef struct { uint16_t limit; gdt_desc_t *gdt; } __attribute__((packed)) gdtr_t; extern void gdt_set_gate(gdt_desc_t gdt[], const int num, const uint32_t base, const uint32_t limit, const uint8_t access, const uint8_t flags); static inline void gdt_load(gdtr_t * const gdtr, const uint16_t codesel, const uint16_t datasel, const bool flush) { /* Dummy variable used as a fake dependency to avoid optimization * reordering of the inline assembly. The flush (if requested) must * always come after we set the GDT, not before */ int dummy; /* load the GDT register */ __asm__ __volatile__ ("lgdt %[gdtr]" : "=X"(dummy) : [gdtr]"m"(*gdtr), /* Dummy constraint to ensure what gdtr->gdt points at is fully * realized into memory before we issue LGDT instruction */ "m"(*(const gdt_desc_t (*)[]) gdtr->gdt)); /* This flushes the selector registers to ensure the new * descriptors are used. */ if (flush) { /* The indirect absolute jump is because we can't * assume that codesel is an immediate value * as it may be passed in a register. We build a * far pointer in memory and indirectly jump through * that pointer. This explicitly sets CS selector */ __asm__ __volatile__ ( "pushl %[codesel]\n\t" "pushl $1f\n\t" "ljmpl *(%%esp)\n" "1:\n\t" "add $8, %%esp\n\t" "mov %[datasel], %%ds\n\t" "mov %[datasel], %%es\n\t" "mov %[datasel], %%ss\n\t" "mov %[datasel], %%fs\n\t" "mov %[datasel], %%gs" : /* No outputs */ : "X"(dummy), [datasel]"r"(datasel), [codesel]"g"((uint32_t)codesel)); } return; } #endif
gdt.c
:
#include "gdt.h" /* Setup a descriptor in the Global Descriptor Table */ void gdt_set_gate(gdt_desc_t gdt[], const int num, const uint32_t base, const uint32_t limit, const uint8_t access, const uint8_t flags) { /* Setup the descriptor base access */ gdt[num].base_low = (base & 0xFFFF); gdt[num].base_middle = (base >> 16) & 0xFF; gdt[num].base_high = (base >> 24) & 0xFF; /* Setup the descriptor limits */ gdt[num].limit_low = (limit & 0xFFFF); gdt[num].flags = ((limit >> 16) & 0x0F); /* Finally, set up the flags and access byte */ gdt[num].flags |= (flags << 4); gdt[num].access = access; }
multiboot.asm
:
%include "gdt.inc" STACKSIZE equ 0x4000 bits 32 global mbentry extern kmain ; Multiboot Header section .multiboot MBALIGN equ 1<<0 MEMINFO equ 1<<1 VIDINFO equ 0<<2 FLAGS equ MBALIGN | MEMINFO | VIDINFO MAGIC equ 0x1BADB002 CHECKSUM equ -(MAGIC + FLAGS) mb_hdr: dd MAGIC dd FLAGS dd CHECKSUM section .text mbentry: cli cld mov esp, stack_top ; EAX = magic number. Should be 0x2badb002 ; EBX = pointer to multiboot_info ; Pass as parameters right to left push eax push ebx call kmain ; Infinite loop to end program cli endloop: hlt jmp endloop section .bss align 32 stack: resb STACKSIZE stack_top:
kernel.c
:
#include#include #include "vesadrv.h" #include "gdt.h" #define CODE32SEL 0x08 #define DATA32SEL 0x10 #define CODE16SEL 0x18 #define DATA16SEL 0x20 #define NUM_GDT_ENTRIES 5 /* You can get this structure from GRUB's multiboot.h if needed * https://www.gnu.org/software/grub/manual/multiboot/html_node/multiboot_002eh.html */ struct multiboot_info; /* Values made available by the linker script */ extern void *__realmode_lma_start; extern void *__realmode_lma_end; extern void *__realmode_vma_start; /* Pointer to graphics memory.Mark as volatile since * video memory is memory mapped IO. Certain optimization * should not be performed. */ volatile uint32_t * video_gfx_ptr; /* GDT descriptor table */ gdt_desc_t gdt[NUM_GDT_ENTRIES]; /* Copy the code and data in the realmode section down into the lower * 64kb of memory @ 0x00001000. */ static void realmode_setup (void) { /* Each of these __realmode* values is generated by the linker script */ uint32_t *src_addr = (uint32_t *)&__realmode_lma_start; uint32_t *dst_addr = (uint32_t *)&__realmode_vma_start; uint32_t *src_end = (uint32_t *)&__realmode_lma_end; /* Copy a DWORD at a time from source to destination */ while (src_addr < src_end) *dst_addr++ = *src_addr++; } void gdt_setup (gdt_desc_t gdt[], const int numdesc) { gdtr_t gdtr = { sizeof(gdt_desc_t)*numdesc-1, gdt }; /* Null descriptor */ gdt_set_gate(gdt, 0, 0x00000000, 0x00000000, 0x00, 0x0); /* 32-bit Code descriptor, flat 4gb */ gdt_set_gate(gdt, 1, 0x00000000, 0xffffffff, 0x9A, 0xC); /* 32-bit Data descriptor, flat 4gb */ gdt_set_gate(gdt, 2, 0x00000000, 0xffffffff, 0x92, 0xC); /* 16-bit Code descriptor, limit 0xffff bytes */ gdt_set_gate(gdt, 3, 0x00000000, 0x0000ffff, 0x9A, 0x0); /* 16-bit Data descriptor, limit 0xffffffff bytes */ gdt_set_gate(gdt, 4, 0x00000000, 0xffffffff, 0x92, 0x8); /* Load global decriptor table, and flush the selectors */ gdt_load(&gdtr, CODE32SEL, DATA32SEL, true); } int kmain(struct multiboot_info *mb_info, const uint32_t magicnum) { struct mode_info_t *pMI; uint32_t pixel_colors = 0; /* Quiet compiler about unused variables */ (void) mb_info; (void) magicnum; /* Setup the GDT */ gdt_setup(gdt, NUM_GDT_ENTRIES); /* Setup real mode code and data */ realmode_setup(); /* Switch to video mode 0x13 (320x200x256) * The physical address of the mode 13 video memory is * 0xa0000 */ pMI = do_vbe(0x13); video_gfx_ptr = pMI->physbase; /* Display part of the VGA palette as a test pattern */ for (int pixelpos = 0; pixelpos < (320*200); pixelpos++) { if ((pixel_colors & 0xff) == (320/4)) pixel_colors = 0; pixel_colors += 0x01010101; video_gfx_ptr[pixelpos] = pixel_colors; } return 0; }
A simple set of commands to assemble/compile/link the code above into an ELF executable called multiboot.elf
:
nasm -f elf32 -g -F dwarf -o multiboot.o multiboot.asm nasm -f elf32 -g -F dwarf -o vesadrv.o vesadrv.asm i686-elf-gcc -std=c99 -g -m32 -O3 -c -fno-exceptions -nostdlib -ffreestanding -Wall -Wextra -o kernel.o kernel.c i686-elf-gcc -std=c99 -g -m32 -O3 -c -fno-exceptions -nostdlib -ffreestanding -Wall -Wextra -pedantic -o gdt.o gdt.c -lgcc i686-elf-gcc -m32 -Tlink.ld -ffreestanding -nostdlib -o multiboot.elf multiboot.o kernel.o gdt.o vesadrv.o -lgcc
You can find a copy of the code above on my site. When I run the kernel in QEMU this is what I see: