我在asm中编写了一个引导加载程序,并希望在我的项目中添加一些已编译的C代码。
我在这里创建了一个测试函数:
test.c的
__asm__(".code16n"); void print_str() { __asm__ __volatile__("mov $'A' , %aln"); __asm__ __volatile__("mov $0x0e, %ahn"); __asm__ __volatile__("int $0x10n"); }
这是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可执行文件。 此文件将来可用于调试目的。 这里我们使用OBJCOPY将kernel.elf
转换为名为kernel.bin
的二进制文件。 kernel.bin
可以用作引导加载程序映像。
您应该能够使用此命令使用QEMU运行它:
qemu-system-i386 -fda kernel.bin
运行时可能看起来像:
你会注意到最后一行出现了字母A
这是我们对print_str
代码的期望。
GCC内联汇编很难做到
如果我们在问题中采用您的示例代码:
__asm__ __volatile__("mov $'A' , %aln"); __asm__ __volatile__("mov $0x0e, %ahn"); __asm__ __volatile__("int $0x10n");
如果需要,编译器可以自由地重新排序这些__asm__
语句。 int $0x10
可能出现在MOV指令之前。 如果您希望以这个确切的顺序输出这3行,您可以将它们组合成如下所示:
__asm__ __volatile__("mov $'A' , %alnt" "mov $0x0e, %ahnt" "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__(".code16gccn"); 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__(".code16n");
语句真的不适合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__(".code16gccn");
在支持-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生成代码存在许多缺点:
如果你想从更现代的C编译器生成真正的16位代码,我推荐使用OpenWatcom C.
零填充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__(".code16gccn");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, %ebxn"); __asm__ __volatile__("mov $1, %eax n"); __asm__ __volatile__("int $0x80n"); } char* tmp; ///how to direct use of arguments in asm command void print_str(int a, char* s){ x = a; __asm__("mov x, %edxn");// ;third argument: message length tmp = s; __asm__("mov tmp, %ecxn");// ;second argument: pointer to message to write __asm__("mov $1, %ebxn");//first argument: file handle (stdout) __asm__("mov $4, %eaxn");//system call number (sys_write) __asm__ __volatile__("int $0x80n");//call kernel } void mtest(){ printf("%sn", "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]
以上就是c/c++开发分享使用外部c代码编译asm引导加载程序相关内容,想了解更多C/C++开发(异常处理)及C/C++游戏开发关注计算机技术网(www.ctvol.com)!)。
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/c-cdevelopment/562266.html