使用外部c代码编译asm引导加载程序

Mar*_*yse 5 x86 gcc nasm inline-assembly bootloader

我在asm中编写了一个引导加载程序,并希望在我的项目中添加一些已编译的C代码.

我在这里创建了一个测试函数:

test.c的

__asm__(".code16\n");

void print_str() {
    __asm__ __volatile__("mov $'A' , %al\n");
    __asm__ __volatile__("mov $0x0e, %ah\n");
    __asm__ __volatile__("int $0x10\n");
}
Run Code Online (Sandbox Code Playgroud)

这是asm代码(引导加载程序):

hw.asm

[org 0x7C00]
[BITS 16]
[extern print_str] ;nasm tip

start:
mov ax, 0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00


mov si, name
call print_string

mov al, ' '
int 10h

mov si, version
call print_string

mov si, line_return
call print_string

call print_str ;call function

mov si, welcome
call print_string

jmp mainloop

mainloop:
mov si, prompt
call print_string

mov di, buffer
call get_str

mov si, buffer
cmp byte [si], 0
je mainloop

mov si, buffer
;call print_string
mov di, cmd_version
call strcmp
jc .version

jmp mainloop

.version:
mov si, name
call print_string

mov al, ' '
int 10h

mov si, version
call print_string

mov si, line_return
call print_string
jmp mainloop

name db 'MOS', 0
version db 'v0.1', 0
welcome db 'Developped by Marius Van Nieuwenhuyse', 0x0D, 0x0A, 0
prompt db '>', 0
line_return db 0x0D, 0x0A, 0
buffer times 64 db 0

cmd_version db 'version', 0

%include "functions/print.asm"
%include "functions/getstr.asm"
%include "functions/strcmp.asm"

times 510 - ($-$$) db 0
dw 0xaa55
Run Code Online (Sandbox Code Playgroud)

我需要调用c函数就像一个简单的asm函数没有extern和调用print_str,在VMWare中启动asm脚本.

我试着编译:

nasm -f elf32 
Run Code Online (Sandbox Code Playgroud)

但我不能打电话给组织0x7C00

Mic*_*tch 11

编译和链接NASM和GCC代码

虽然有可能,但这个问题的答案比人们想象的要复杂得多.引导加载程序的第一阶段(在物理地址0x07c00处加载的原始512字节)是否可以调用C函数?是的,但需要重新思考如何构建项目.

为了实现这一点,你不能再-f bin使用NASM了.这也意味着你不能使用它org 0x7c00告诉汇编器代码期望从哪个地址开始.您需要通过链接器(直接使用我们的LD或使用GCC进行链接)来完成此操作.由于链接器会将内容放在内存中,因此我们不能依赖将引导扇区签名0xaa55放在输出文件中.我们可以让链接器为我们这样做.

您将发现的第一个问题是GCC内部使用的默认链接器脚本不会按照我们想要的方式进行布局.我们需要创建自己的.这样的链接描述文件必须将原点(虚拟内存地址又名VMA)设置为0x7c00,将数据之前的汇编文件中的代码放在文件中,并将引导签名放在偏移量510处.我不会写关于链接器脚本的教程.该Binutils的文档包含了几乎所有你需要知道的连接器脚本的一切.

OUTPUT_FORMAT("elf32-i386");
/* We define an entry point to keep the linker quiet. This entry point
 * has no meaning with a bootloader in the binary image we will eventually
 * generate. Bootloader will start executing at whatever is at 0x07c00 */
ENTRY(start);
SECTIONS
{
    . = 0x7C00;
    .text : {
        /* Place the code in hw.o before all other code */
        hw.o(.text);
        *(.text);
    }

    /* Place the data after the code */
    .data : SUBALIGN(4) {
        *(.data);
        *(.rodata);
    }

    /* Place the boot signature at VMA 0x7DFE */
    .sig : AT(0x7DFE) {
        SHORT(0xaa55);
    }

    /* Place the uninitialised data in the area after our bootloader
     * The BIOS only reads the 512 bytes before this into memory */
    . = 0x7E00;
    .bss : SUBALIGN(4) {
        __bss_start = .;
        *(COMMON);
        *(.bss)
        . = ALIGN(4);
        __bss_end = .;
    }
    __bss_sizeb = SIZEOF(.bss);

    /* Remove sections that won't be relevant to us */
    /DISCARD/ : {
        *(.eh_frame);
        *(.comment);
        *(.note.gnu.build-id);
    }
}
Run Code Online (Sandbox Code Playgroud)

此脚本应创建一个ELF可执行文件,可以使用OBJCOPY将其转换为平面二进制文件.我们可以直接输出二进制文件,但是如果我想在ELF版本中包含调试信息以用于调试目的,我将两个进程分开.

现在我们有了一个链接器脚本,我们必须删除它ORG 0x7c00和引导签名.为简单起见,我们将尝试使用以下代码(hw.asm):

extern print_str
global start
bits 16

section .text
start:
xor ax, ax         ; AX = 0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00

call print_str     ; call function

/* Halt the processor so we don't keep executing code beyond this point */
cli
hlt
Run Code Online (Sandbox Code Playgroud)

您可以包含所有其他代码,但此示例仍将演示调用C函数的基础知识.

假设上面的代码现在可以生成ELF从对象hw.asm产生hw.o使用该命令:

nasm -f elf32 hw.asm -o hw.o
Run Code Online (Sandbox Code Playgroud)

您使用以下内容编译每个C文件:

gcc -ffreestanding -c kmain.c -o kmain.o
Run Code Online (Sandbox Code Playgroud)

我将你的C代码放入一个名为的文件中kmain.c.上面的命令将生成kmain.o.我注意到你没有使用交叉编译器,因此你需要使用它-fno-PIE来确保我们不生成可重定位代码.-ffreestanding告诉GCC C标准库可能不存在,main可能不是程序入口点.您将以相同的方式编译每个C文件.

要将此代码链接到最终的可执行文件,然后生成可以启动的平面二进制文件,我们这样做:

ld -melf_i386 -T link.ld kmain.o hw.o -o kernel.elf
objcopy -O binary kernel.elf kernel.bin
Run Code Online (Sandbox Code Playgroud)

您指定要与LD命令链接的所有目标文件.上面的LD命令将生成一个名为32位的ELF可执行文件kernel.elf.此文件将来可用于调试目的.这里我们使用OBJCOPY转换kernel.elf为一个名为的二进制文件kernel.bin.kernel.bin可以用作引导加载程序映像.

您应该能够使用此命令使用QEMU运行它:

qemu-system-i386 -fda kernel.bin
Run Code Online (Sandbox Code Playgroud)

运行时可能看起来像:

在此输入图像描述

你会注意到这封信A出现在最后一行.这是我们对print_str代码的期望.


GCC内联汇编很难做到

如果我们在问题中采用您的示例代码:

__asm__ __volatile__("mov $'A' , %al\n");
__asm__ __volatile__("mov $0x0e, %ah\n");
__asm__ __volatile__("int $0x10\n");
Run Code Online (Sandbox Code Playgroud)

如果需要,编译器可以自由地重新排序这些__asm__语句.本int $0x10可以在MOV指令之前出现.如果您希望以这个确切的顺序输出这3行,您可以将它们组合成如下所示:

__asm__ __volatile__("mov $'A' , %al\n\t"
                     "mov $0x0e, %ah\n\t"
                     "int $0x10");
Run Code Online (Sandbox Code Playgroud)

这些是基本的汇编语句.它们不需要指定__volatile__它们,因为它们已经隐式挥发,所以它没有效果.从原始海报的答案可以清楚地看出,他们最终希望在__asm__块中使用变量.这对于扩展的内联汇编是可行的(指令字符串后跟冒号:后跟约束.):

使用扩展的asm,您可以从汇编程序读取和写入C变量,并执行从汇编程序代码到C标签的跳转.扩展的asm语法使用冒号(':')来分隔汇编程序模板后的操作数参数:

asm [volatile] ( AssemblerTemplate
                : OutputOperands 
                [ : InputOperands
                [ : Clobbers ] ])
Run Code Online (Sandbox Code Playgroud)

这个答案不是关于内联汇编的教程.一般的经验法则是,除非必须,否则不应使用内联汇编.内联汇编错误可能会导致很难跟踪错误或产生不寻常的副作用.不幸的是,在C中执行16位中断几乎需要它,或者你在汇编中编写整个函数(即:NASM).

这是一个print_chr函数的示例,它接受一个空终止字符串并使用Int 10h/ah = 0ah逐个打印每个字符:

#include <stdint.h>
__asm__(".code16gcc\n");

void print_str(char *str) {
    while (*str) {
        /* AH=0x0e, AL=char to print, BH=page, BL=fg color */
        __asm__ __volatile__ ("int $0x10"
                              :
                              : "a" ((0x0e<<8) | *str++),
                                "b" (0x0000));
    }
}
Run Code Online (Sandbox Code Playgroud)

hw.asm 将被修改为如下所示:

push welcome
call print_str ;call function
Run Code Online (Sandbox Code Playgroud)

组装/编译(使用本答案第一部分中的命令)并运行时的想法是打印出welcome消息.不幸的是,它几乎永远不会工作,甚至可能会崩溃像QEMU这样的模拟器.


code16几乎无用,不应该使用

在上一节中,我们了解到一个带参数的简单函数最终无法正常运行,甚至可能导致像QEMU这样的仿真器崩溃.主要问题是该__asm__(".code16\n");语句对GCC生成的代码效果不佳.该Binutils的AS文件说:

'.code16gcc'为从gcc生成16位代码提供了实验支持,与'.'调用','ret','enter','leave','push','pop',''. pusha','popa','pushf'和'popf'指令默认为32位大小.这使得堆栈指针在函数调用上以相同的方式被操纵,允许在与32位模式相同的堆栈偏移处访问函数参数.'.code16gcc'还会在必要时自动添加地址大小前缀,以使用gcc生成的32位寻址模式.

.code16gcc是你真正需要使用的,而不是.code16.这个强制后端的GNU汇编器在某些指令上发出地址和操作数前缀,这样地址和操作数被视为4字节宽,而不是2字节.

NASM中的手写代码不知道它将调用C指令,NASM也没有像这样的指令.code16gcc.您需要修改汇编代码,以便在实模式下将32位值压入堆栈.您还需要覆盖call指令,以便返回地址需要被视为32位值,而不是16位.这段代码:

push welcome
call print_str ;call function
Run Code Online (Sandbox Code Playgroud)

应该:

    jmp 0x0000:setcs
setcs:
    cld
    push dword welcome
    call dword print_str ;call function
Run Code Online (Sandbox Code Playgroud)

GCC要求在调用任何C函数之前清除方向标志.我将CLD指令添加到汇编代码的顶部以确保是这种情况.GCC代码还需要CS到0x0000才能正常工作.该FAR JMP做到了这一点.

您也可以放弃支持该选项的__asm__(".code16gcc\n");现代GCC-m16.-m16自动将a .code16gcc放入正在编译的文件中.

由于GCC还使用完整的32位堆栈指针,因此最好使用0x7c00 初始化ESP,而不仅仅是SP.更改mov sp, 0x7C00mov esp, 0x7C00.这可确保完整的32位堆栈指针为0x7c00.

修改后的kmain.c代码现在应该如下所示:

#include <stdint.h>

void print_str(char *str) {
    while (*str) {
        /* AH=0x0e, AL=char to print, BH=page, BL=fg color */
        __asm__ __volatile__ ("int $0x10"
                              :
                              : "a" ((0x0e<<8) | *str++),
                                "b" (0x0000));
    }
}
Run Code Online (Sandbox Code Playgroud)

并且hw.asm:

extern print_str
global start
bits 16

section .text
start:
    xor ax, ax            ; AX = 0
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov esp, 0x7C00
    jmp 0x0000:setcs      ; Set CS to 0
setcs:
    cld                   ; GCC code requires direction flag to be cleared 

    push dword welcome
    call dword print_str  ; call function
    cli
    hlt

section .data
welcome db 'Developped by Marius Van Nieuwenhuyse', 0x0D, 0x0A, 0
Run Code Online (Sandbox Code Playgroud)

这些命令可以构建引导加载程序:

gcc -fno-PIC -ffreestanding -m16 -c kmain.c -o kmain.o 
ld -melf_i386 -T link.ld kmain.o hw.o -o kernel.elf
objcopy -O binary kernel.elf kernel.bin
Run Code Online (Sandbox Code Playgroud)

qemu-system-i386 -fda kernel.bin它一起运行应该看起来类似于:

在此输入图像描述


在大多数情况下,GCC生成需要80386+的代码

GCC生成的代码有许多缺点.code16gcc:

  • ES = DS = CS = SS必须为0
  • 代码必须符合前64kb
  • GCC代码不了解20位段:偏移寻址.
  • 除了最简单的C代码之外,GCC不会生成可以在286/186/8086上运行的代码.它以实模式运行,但它使用32位操作数,并且在80386之前的处理器上不提供寻址.
  • 如果你想访问第一个64kb以上的内存位置,那么在调用C代码之前你需要处于虚幻模式(大).

如果你想从更现代的C编译器生成真正的16位代码,我推荐使用OpenWatcom C.

  • 内联汇编不如GCC强大
  • 内联汇编语法不同,但它比GCC的内联汇编更容易使用且更不容易出错.
  • 可以生成将在陈旧的8086/8088处理器上运行的代码.
  • 理解20位段:偏移实模式寻址,并支持远大指针的概念.
  • wlink Watcom链接器可以生成可用作引导加载程序的基本平面二进制文件.

零填充BSS部分

BIOS启动顺序不保证内存实际上为零.这导致零初始化区域BSS的潜在问题.在第一次调用C代码之前,区域应该由汇编代码填充零.我最初编写的链接描述文件定义了一个符号__bss_start,它是BSS内存的偏移量,__bss_sizeb是以字节为单位的大小.使用此信息,您可以使用STOSB指令轻松将其填充.在顶部hw.asm你可以添加:

extern __bss_sizeb
extern __bss_start
Run Code Online (Sandbox Code Playgroud)

CLD指令之后和调用任何C代码之前,你可以这样做零填充:

; Zero fill the BSS section
mov cx, __bss_sizeb       ; Size of BSS computed in linker script
mov di, __bss_start       ; Start of BSS defined in linker script
rep stosb                 ; AL still zero, Fill memory with zero
Run Code Online (Sandbox Code Playgroud)

其他建议

为了减少编译器生成的代码膨胀,可以使用它-fomit-frame-pointer.编译-Os可以优化空间(而不是速度).我们为BIOS加载的初始代码提供了有限的空间(512字节),因此这些优化可能是有益的.用于编译的命令行可能显示为:

gcc -fno-PIC -fomit-frame-pointer -ffreestanding -m16 -Os -c kmain.c -o kmain.o
Run Code Online (Sandbox Code Playgroud)