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

Posted

技术标签:

【中文标题】将 16 位实模式代码链接到符合 Multiboot 的 ELF 可执行文件时出现 LD 错误【英文标题】:LD errors while linking 16-bit real mode code into a Multiboot compliant ELF executable 【发布时间】:2017-05-24 15:36:31 【问题描述】:

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

重定位被截断以适应: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

我还用“链接器错误”注释了这些行

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

【问题讨论】:

好的,我将添加我使用的命令并尝试使用 [dword vid_mode] 虽然这不会修复 jmp 0x8:pm1。另请注意,我没有使用 grub 我使用的是 syslinux 好的,使用 32 位寻址解决了链接问题,但现在它出现了三重错误,所以现在我只需要解决这个问题 :) @MichaelPetch 使用[dword vid_mode] 将解决链接问题,但在执行时会产生一般保护错误,因为vid_mode 的地址超出了0xFFFF 实模式段限制。 @MichaelPetch 我认为虚幻模式在这种情况下不可行,因为他正在使用 Bios 调用,这会将段限制重置为其正常的实模式值。更好的解决方案是将实模式代码和vid_mode 变量复制到实模式可以正常访问的地方。 @RossRidge:我同意。我在我早期的一个 cmets 下放了一个注释通常我将预期在实模式下使用的代码放在较低的内存中(如果它在前 64kb 中,事情会容易得多)。 .任何低于 1mb 的东西都可以工作( 【参考方案1】:

链接器错误的原因

您收到此错误:

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

实际上是在告诉您,当链接器尝试在 .text 部分中解析这些重定位时,它无法这样做,因为它计算的虚拟内存地址 (VMA) 无法放入 16 位指针 (@ 987654333@).

如果您在与 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 来覆盖默认地址和操作数大小。绝对 32 位 FAR JMP 可以通过在操作数前指定 DWORD 进行编码。这些行:

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 Mode。 OSDev Wiki 有一些代码可以用作这种支持的基础。假设 PIC 没有重新映射并且处于初始配置中,那么实现按需虚幻模式的常用方法是将 0x0d 中断处理程序替换为以下内容:

    查询PIC1OCW3 以查看是否正在服务IRQ5 或是否存在一般保护故障。如果没有 PIC 重新映射 #GP 故障和 IRQ5 指向同一个中断向量,因此必须区分它们。 如果设置了 IRQ5 ISR,则调用之前保存的中断处理程序(链接)。至此,我们都完成了。 如果未设置 IRQ5 ISR,则由于一般保护故障而调用了 0x0d 中断。假设故障是由于无效的数据访问造成的。 切换到保护模式并使用包含 16 位数据描述符的 GDT,该描述符具有 0 基数和 0xffffffff 限制。使用相应的选择器设置 ESDS。 离开保护模式 从中断处理程序返回。

如果 PIC1 已重新映射以不与 x86 异常处理中断(int 0x08 到 int 0x0f)冲突,则步骤 1、2、3 不再适用。 Remapping the PICs 避免这种冲突在 x86 操作系统设计中很常见。问题中的代码不执行任何 PIC 重新映射。

如果您想在 VM8086 任务中使用代码而不是进入实模式,则此机制将不起作用。

DOS 的 HIMEM.SYS 在 1980 年代做了类似的事情,如果您有兴趣,可以在 article 中找到关于此的讨论。

注意:虽然我给出了使用虚幻模式的一般描述,但我不推荐这种方法。它需要更广泛的实模式、保护模式、中断处理知识。


更优选的解决方案

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

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

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

请参阅最后一节中的代码,了解执行此操作的链接器脚本 link.ld 和执行复制的 kernel.c 文件。

使用适当调整的 VESA 代码,您尝试做的事情应该可以工作。


您发现的 VESA 代码问题

VESA code 依赖于硬编码地址 在设计时并未考虑 Multiboot,因为它假定内核将在特定位置手动加载到内存 ( 代码不遵循CDECL 调用约定,因此不能直接从C 代码调用。 存在将 32 位代码置于 [bits 16] 指令下的错误。 代码没有显示所需的 GDT 表,但可以从代码中推断出 至少 5 个描述符在 GDT 按特定顺序。
    空描述符 32 位代码段(Base 0,Limit 0xffffffff)。选择器 0x08 32 位数据段(Base 0,Limit 0xffffffff)。选择器 0x10 16 位代码段(基数为 0,限制至少为 0xffff)。选择器 0x18 16 位数据段(基数为 0,限制至少为 0xffff)。选择器 0x20

作者有这些cmets:

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


一个完整的例子

以下代码是上述建议的完整实现。使用链接描述文件生成实模式代码和数据,并将其放置在从 0x1000 开始的位置。该代码使用C 来设置一个适当的GDT,其中包含32 位和16 位代码和数据段,将实模式代码从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
    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 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)
    
    /* load the GDT register */
    __asm__ __volatile__ ("lgdt %[gdtr]"
                          :
                          : [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 */
                 : [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 <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, 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;

一组简单的命令将上面的代码组装/编译/链接到一个名为multiboot.elfELF可执行文件中:

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 中运行内核时,我看到的是这样的:

【讨论】:

以上是关于将 16 位实模式代码链接到符合 Multiboot 的 ELF 可执行文件时出现 LD 错误的主要内容,如果未能解决你的问题,请参考以下文章

解决win7系统不支持16位实模式汇编程序DOS执行的问题

在 16 位裸机 nasm 组件中休眠 x 毫秒

Linux内核从开机加电到main函数执行

「30天制作操作系统系列」5~8天C语言处理鼠标键盘与中断

「30天制作操作系统系列」5~8天C语言处理鼠标键盘与中断

「30天制作操作系统系列」5~8天C语言处理鼠标键盘与中断