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

我在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"); } 

这是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 

我需要像简单的asm函数一样调用c函数而不使用extern和调用print_str ,在VMWare中启动asm脚本。

我试着编译:

 nasm -f elf32 

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

编译和链接NASM和GCC代码

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

为了实现这一目标,你不能再使用NAS- -f bin了。 这也意味着你不能使用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); } } 

此脚本应创建一个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 

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

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

 nasm -f elf32 hw.asm -o hw.o 

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

 gcc -ffreestanding -c kmain.c -o kmain.o 

我将您的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 

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

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

 qemu-system-i386 -fda kernel.bin 

运行时可能看起来像:

在此处输入图像描述

你会注意到最后一行出现了字母A 这是我们对print_str代码的期望。


GCC内联汇编很难做到

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

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

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

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

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

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

 asm [volatile] ( AssemblerTemplate : OutputOperands [ : InputOperands [ : Clobbers ] ]) 

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

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

 #include  __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)); } } 

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

 push welcome call print_str ;call function 

组装/编译(使用本答案第一部分中的命令)并运行时的想法是打印出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 

应该:

  jmp 0x0000:setcs setcs: cld push dword welcome call dword print_str ;call function 

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

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

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

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

 #include  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)); } } 

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 

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

 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 

当使用qemu-system-i386 -fda kernel.bin运行时,它看起来应该qemu-system-i386 -fda kernel.bin

在此处输入图像描述


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

使用.code16gcc GCC生成代码存在许多缺点:

  • 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 

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 

其他建议

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

 gcc -fno-PIC -fomit-frame-pointer -ffreestanding -m16 -Os -c kmain.c -o kmain.o 

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

然后,您需要使用16位x86编译器,例如OpenWatcom 。

GCC无法安全地构建实模式代码 ,因为它不知道平台的一些重要function,包括内存分段。 插入.code16指令将使编译器生成错误的输出。 尽管出现在许多教程中,但这条建议完全不正确,不应该使用。

首先,我想表达如何将C编译代码与汇编文件链接起来。

我在SO中汇总了一些Q / A并达到了这一目标。

C代码:

func.c

 //__asm__(".code16gcc\n");when we use eax, 32 bit reg we cant use this as truncate //problem #include  int x = 0; int madd(int a, int b) { return a + b; } void mexit(){ __asm__ __volatile__("mov $0, %ebx\n"); __asm__ __volatile__("mov $1, %eax \n"); __asm__ __volatile__("int $0x80\n"); } char* tmp; ///how to direct use of arguments in asm command void print_str(int a, char* s){ x = a; __asm__("mov x, %edx\n");// ;third argument: message length tmp = s; __asm__("mov tmp, %ecx\n");// ;second argument: pointer to message to write __asm__("mov $1, %ebx\n");//first argument: file handle (stdout) __asm__("mov $4, %eax\n");//system call number (sys_write) __asm__ __volatile__("int $0x80\n");//call kernel } void mtest(){ printf("%s\n", "Hi"); //putchar('a');//why not work } ///gcc -c func.c -o func 

汇编代码:

hello.asm

 extern mtest extern printf extern putchar extern print_str extern mexit extern madd section .text ;section declaration ;we must export the entry point to the ELF linker or global _start ;loader. They conventionally recognize _start as their ;entry point. Use ld -e foo to override the default. _start: ;write our string to stdout push msg push len call print_str; call mtest ;print "Hi"; call printf inside a void function ; use add inside func.c push 5 push 10 call madd; ;direct call of  printf() push eax push format call printf; ;printf(format, eax) call mexit; ;exit to OS section .data ;section declaration format db "%d", 10, 0 msg db "Hello, world!",0xa ;our dear string len equ $ - msg ;length of our dear string ; nasm -f elf32 hello.asm -o hello ;Link two files ;ld hello func -o hl -lc -I /lib/ld-linux.so.2 ; ./hl run code ;chain to assemble, compile, Run ;; gcc -c func.c -o func && nasm -f elf32 hello.asm -o hello && ld hello func -o hl -lc -I /lib/ld-linux.so.2 && echo &&./hl 

用于汇编,编译和运行的链命令

gcc -c func.c -o func && nasm -f elf32 hello.asm -o hello && ld hello func -o hl -lc -I /lib/ld-linux.so.2 && echo && ./hl

编辑[TODO]

  • 编写引导加载程序代码而不是此版本
  • 关于ld,gcc,nasm如何工作的一些解释。