gpt4 book ai didi

gcc - 使用外部C代码编译ASM引导加载程序

转载 作者:行者123 更新时间:2023-12-03 19:54:55 25 4
gpt4 key购买 nike

我在asm中编写了一个引导加载程序,并想在我的项目中添加一些已编译的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代码(引导加载程序):

硬件

[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 


但我无法致电org 0x7C00

最佳答案

编译和链接NASM和GCC代码

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

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

您会发现的第一个问题是,GCC内部使用的默认链接程序脚本并未按照我们想要的方式进行布局。我们需要创建自己的。这种链接描述文件必须将原点(虚拟内存地址,又称VMA)设置为0x7c00,将程序集文件中的代码放置在数据之前,并将启动签名放置在文件中的偏移量510处。我不会写有关链接程序脚本的教程。 Binutils Documentation包含您几乎需要了解的有关链接描述文件的所有内容。

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(2) {
*(.data);
*(.rodata*);
}

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

/* Place the uninitialised data in the area after our bootloader
* The BIOS only reads the 512 bytes before this into memory */
.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);
}
}


该脚本应创建一个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 --build-id=none -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


运行时,它可能看起来像:

enter image description here

您会发现字母 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指令之前。如果要以完全相同的顺序输出这三行,可以将它们组合成这样:

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


这些是基本的汇编语句。不需要在它们上指定 __volatile__,因为它们已经是 implicitly volatile,因此没有效果。从原始发布者的答案中可以明显看出,他们最终希望在 __asm__块中使用变量。这对于 extended inline assembly是可行的(指令字符串后跟冒号 :,然后是约束。):

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


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



这个答案不是关于内联汇编的教程。一般的经验法则是一个 should not use inline assembly unless you have to。内联汇编做错了会造成难以跟踪的错误或异常的副作用。不幸的是,在C中执行16位中断几乎需要它,或者您在汇编中编写了整个函数(即:NASM)。

这是 print_chr函数的示例,该函数采用以nul结尾的字符串,并使用 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));
}
}


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

push welcome
call print_str ;call function


组装/编译(使用此答案第一部分中的命令)并运行时的想法是,它会打印出 welcome消息。不幸的是,它几乎永远无法工作,甚至可能使某些仿真器(如QEMU)崩溃。



code16几乎无用,不应使用

在上一节中,我们了解到带有参数的简单函数最终无法正常工作,甚至可能使QEMU之类的模拟器崩溃。主要问题是 __asm__(".code16\n");语句实际上与GCC生成的代码不能很好地配合使用。 Binutils AS documentation说:


'.code16gcc'提供了从gcc生成16位代码的实验性支持,与'.code16'的不同之处在于'call','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");选项的现代GCC上放置 -m16-m16自动将 .code16gcc放入正在编译的文件中。

由于GCC还使用完整的32位堆栈指针,因此最好使用0x7c00而不是SP初始化ESP。将 mov sp, 0x7C00更改为 mov 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));
}
}


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 --build-id=none -T link.ld kmain.o hw.o -o kernel.elf
objcopy -O binary kernel.elf kernel.bin


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

enter image description here



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

使用 .code16gcc的GCC生成的代码有很多缺点:


ES = DS = CS = SS必须为0
代码必须适合前64kb
GCC代码不了解20位segment:offset寻址。
除了最琐碎的C代码外,GCC不会生成可以在286/186/8086上运行的代码。它在实模式下运行,但使用32位操作数,且寻址在80386之前的处理器上不可用。
如果要访问前64kb以上的内存位置,则在调用C代码之前必须位于 Unreal Mode(big)中。


如果要使用更现代的C编译器生成真实的16位代码,我建议 OpenWatcom C


内联汇编不如GCC强大
内联汇编语法不同,但与GCC内联汇编相比,它更易于使用且更不易出错。
可以生成将在过时的8086/8088处理器上运行的代码。
理解20位segment:offset实模式寻址,并支持远大指针的概念。
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编译可以优化空间(而不是速度)。 BIOS加载的初始代码的空间有限(512字节),因此这些优化可能会有所帮助。用于编译的命令行可能显示为:

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

关于gcc - 使用外部C代码编译ASM引导加载程序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47249699/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com