将16位实模式代码链接到符合Multiboot的ELF可执行文件时出现LD错误

时间:2017-01-10 08:01:39

标签: assembly x86 nasm ld osdev

我正在编写一个包含32位内核的Multiboot兼容 ELF 可执行文件。我的主要问题是我在生成可执行文件时收到一系列链接器错误:

  

重定位被截断以适合:R_386_16对.`text'

链接器脚本,代码和构建脚本

我决定尝试在我的操作系统中实现VESA VBE图形。我在OSDev forum中找到了一个现有的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

我还用" Linker错误"

评论了这些行

这是文件(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

1 个答案:

答案 0 :(得分:6)

链接器错误的原因

您收到此错误:

  

重定位被截断以适合:R_386_16与`.text&#39;

有效地告诉您,当链接器尝试在.text部分内解析这些重定位时,它无法解决这些重定位,因为它计算的虚拟内存地址(VMA)不适合16位指针(_16)。

如果在使用 NASM 进行汇编时使用-g -Fdwarf,则可以使用i686-elf-objdump -SDr -Mi8086 vesa.o等命令从 OBJDUMP 生成更多可用输出。

  • -S输出来源
  • -D用于反汇编,
  • -r显示重定位信息。

以下是我得到的输出(略有不同,但这里提出的想法仍然适用):

0000004a <realmode1>:

[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位指针中的最大值。这就是链接器抱怨的原因。

您可以通过在括号[]之间的标签名称前指定 DWORD 来覆盖默认地址和操作数大小。可以通过在操作数之前指定 DWORD 来编码绝对32位 FAR JMP 。这些行:

mov ax,[vid_mode]
mov cx,[vid_mode]
mov bx,[vid_mode]
jmp 0x8:pm1

会变成:

mov ax,[dword vid_mode]
mov cx,[dword vid_mode]
mov bx,[dword vid_mode]
jmp dword 0x8:pm1

如果您组装修改后的代码并使用 OBJDUMP ,如上所述,您将获得此输出(为简洁而剪切):

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

指令现在添加了0x660x67前缀,地址在指令中占用4个字节。每个重定位都是R_386_32类型,它告诉链接器要重定位的地址是32位宽。

虽然上一节中的更改将消除链接期间的警告,但运行时可能无法按预期工作(包括崩溃)。在80386+上,您可以生成16位实模式代码,该代码使用32位地址作为数据,但CPU必须进入允许此类访问的模式。允许通过 DS 段访问的值为0xFFFF以上的32位指针的模式称为Unreal ModeOSDev Wiki有一些代码可以作为此类支持的基础。假设PIC没有重新映射并处于初始配置状态,那么实现按需虚幻模式的通常方法是用以下内容替换0x0d中断处理程序:

  1. 查询PIC1 OCW3以查看IRQ5是否正在服务或是否存在常规保护错误。没有PIC重映射#GP故障和IRQ5指向同一个中断向量,所以必须区分它们。
  2. 如果设置了IRQ5 ISR,则调用先前保存的中断处理程序(链接)。在这一点上,我们都完成了。
  3. 如果未设置IRQ5 ISR,则由于一般保护错误而调用0x0d中断。假设故障是由于数据访问无效。
  4. 切换到保护模式并使用包含16位数据描述符的GDT,其基数为0,限制为0xffffffff。使用相应的选择器设置 ES DS
  5. 离开保护模式
  6. 从中断处理程序返回。
  7. 如果已重新映射PIC1以便不与x86异常处理中断冲突(int 0x08到int 0x0f),则步骤1,2,3不再适用。 Remapping the PICs避免这种冲突在x86 OS设计中很常见。问题中的代码不进行任何PIC重映射。

    如果您想在VM8086任务中使用代码而不是进入实模式,这种机制将无法运行。

    DOS的HIMEM.SYS在20世纪80年代做了类似的事情,如果你感兴趣,你可以在article找到关于它的讨论。

    注意:虽然我提供了使用虚幻模式的一般说明,但我不推荐使用此方法。它需要更多的实模式,保护模式,中断处理知识。

    更优选的解决方案

    不是使用大于0xFFFF的32位数据指针,并确保处理器处于虚幻模式,而是有一个可能更容易理解的解决方案。一种这样的解决方案是将实际模式代码和数据从多路径加载器物理加载到0x100000以上的RAM中复制到实模式中断向量表(IVT)正上方的第一个64KB内存中。这允许您继续使用16位指针,因为第一个64KB的内存可通过16位指针(0x0000到0xFFFF)进行寻址。如果需要,32位代码仍然可以访问实模式数据。

    为此,您必须创建一个更复杂的GNU LD linker scriptlink.ld),它在较低内存中使用虚拟内存地址(原点)。地址0x01000是个不错的选择。 Multiboot标头仍然必须出现在 ELF 可执行文件的开头。

    必须克服的一个问题是Multiboot加载程序会将代码和数据读入0x100000以上的内存中。在可以使用实模式代码之前,必须手动将16位实模式代码和数据复制到地址0x01000。链接描述文件可以帮助生成符号以计算此类副本的起始和结束地址。

    请参阅上一节中的代码,了解执行此操作的链接描述文件link.ld以及执行复制的kernel.c文件。

    通过适当调整的VESA代码,您尝试做的事情应该有效。

    您找到的VESA代码存在问题

    • VESA code依赖于硬编码地址
    • 没有设计Multiboot,因为假设内核将被手动加载到特定位置的内存(<64KB)中,并且某些地址在被调用之前已经包含特定数据。
    • 代码不遵循CDECL调用约定,因此无法直接从 C 代码调用。
    • 有一个错误将32位代码置于[bits 16]指令下。
    • 代码没有显示所需的 GDT 表,但可以从代码中推断出需要至少 5个描述符 GDT 按特定顺序排列。
      1. Null Descriptor
      2. 32位代码段(Base 0,Limit 0xffffffff)。选择器0x08
      3. 32位数据段(基数0,限制0xffffffff)。选择器0x10
      4. 16位代码段(基数0,至少限制为0xffff)。选择器0x18
      5. 16位数据段(基数0,至少限制为0xffff)。选择器0x20

    作者有这些评论:

      

    启动时,使用sidt指令将实模式IDT保存在已知位置(我的位于0x9000),并且不要覆盖内存中的地址0-0x500。它还假设您使用8和16作为PMode中的代码和数据的段寄存器。它将函数4f01的结果存储在0x5000,并自动设置第13位(使用帧缓冲区)

    完整示例

    以下代码是上面建议的完整实现。使用链接描述文件并生成实模式代码和数据,并将其从0x1000开始。代码使用 C 来设置具有32位和16位代码和数据段的正确 GDT ,将实际模式代码从0x100000以下复制到0x1000。它还修复了之前在VESA驱动程序代码中确定的其他问题。为了测试它,切换到视频模式0x13(320x200x256)并一次将部分VGA调色板绘制到显示器32位。

    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, and my favorite cup size
        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
    
    #include <stdint.h>
    
    extern 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 <stdint.h>
    #include <stdbool.h>
    
    typedef struct
    {
            unsigned short limit_low;
            unsigned short base_low;
            unsigned char base_middle;
            unsigned char access;
            unsigned char granularity;
            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 gran);
    
    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 gran)
    {
            /* 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].granularity = ((limit >> 16) & 0x0F);
    
            /* Finally, set up the granularity and access flags */
            gdt[num].granularity |= (gran & 0xF0);
            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 <stdint.h>
    #include <stdbool.h>
    #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, 0x00);
        /* 32-bit Code descriptor, flat 4gb */
        gdt_set_gate(gdt, 1, 0x00000000, 0xffffffff, 0x9A, 0xC0);
        /* 32-bit Data descriptor, flat 4gb */
        gdt_set_gate(gdt, 2, 0x00000000, 0xffffffff, 0x92, 0xC0);
        /* 16-bit Code descriptor, limit 0xffff bytes */
        gdt_set_gate(gdt, 3, 0x00000000, 0x0000ffff, 0x9A, 0x00);
        /* 16-bit Data descriptor, limit 0xffffffff bytes */
        gdt_set_gate(gdt, 4, 0x00000000, 0xffffffff, 0x92, 0x8f);
    
        /* 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;
    }
    

    一组简单的命令,用于将上面的代码汇编/编译/链接到名为multiboot.elf 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
    

    您可以在my site上找到上述代码的副本。当我在 QEMU 中运行内核时,这就是我所看到的:

    enter image description here