我想在我的c代码中实现头文件,其中部分包含16位实模式的 GCC 内联汇编代码,但我似乎有链接问题。这就是我的头文件console.h
的样子:
#ifndef CONSOLE_H
#define CONSOLE_H
extern void kprintf(char*);
#endif
这是console.c
:
#include "console.h"
void kprintf(char *string)
{
for(int i=0;string[i]!='\0';i++)
{
asm("mov $0x0e,%%ah;"
"mov $0x00,%%bh;"
"mov %0,%%al;"
"int $0x10"::"g"(string[i]):"eax", "ebx");
}
}
最后一个hellworld.c
:
asm("jmp main");
#include "console.h"
void main()
{
asm("mov $0x1000,%ax;"
"mov %ax,%es;"
"mov %ax,%ds");
char string[]="hello world";
kprintf(string);
asm(".rept 512;"
"hlt;"
".endr");
}
我的引导程序位于bootloader.asm
:
org 0x7c00
bits 16
section .text
mov ax,0x1000
mov ss,ax
mov sp,0x000
mov esp,0xfffe
xor ax,ax
mov es,ax
mov ds,ax
mov [bootdrive],dl
mov bh,0
mov bp,zeichen
mov ah,13h
mov bl,06h
mov al,1
mov cx,6
mov dh,010h
mov dl,01h
int 10h
load:
mov dl,[bootdrive]
xor ah,ah
int 13h
jc load
load2:
mov ax,0x1000
mov es,ax
xor bx,bx
mov ah,2
mov al,1
mov cx,2
xor dh,dh
mov dl,[bootdrive]
int 13h
jc load2
mov ax,0
mov es,ax
mov bh,0
mov bp,zeichen3
mov ah,13h
mov bl,06h
mov al,1
mov cx,13
mov dh,010h
mov dl,01h
int 10h
mov ax,0x1000
mov es,ax
mov ds,ax
jmp 0x1000:0x000
zeichen db 'hello2'
zeichen3 db 'soweit so gut'
bootdrive db 0
times 510 - ($-$$) hlt
dw 0xaa55
现在我使用以下构建文件build.sh
:
#!bin/sh
nasm -f bin bootloader.asm -o bootloader.bin
gcc hellworld.c -m16 -c -o hellworld.o -nostdlib -ffreestanding
gcc console.c -m16 -c -o console.o -nostdlib link.ld -ffreestanding
ld -melf_i386 -Ttext=0x0000 console.o hellworld.o -o hellworld.elf
objcopy -O binary hellworld.elf hellworld.bin
cat bootloader.bin hellworld.bin >disk.img
qemu-system-i386 disk.img
和linkscript link.ld
:
/*
* link.ld
*/
OUTPUT_FORMAT(elf32-i386)
SECTIONS
{
. = 0x0000;
.text : { *(.startup); *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
}
不幸的是,它不起作用,因为它没有打印预期的hello world
。我认为链接命令一定有问题:
ld -melf_i386 -Ttext=0x0000 console.o hellword.o link.ld -o hellworld.elf`
如何正确链接16位模式的头文件?
当我直接在kprintf
中编写hellworld.c
函数时,它正常工作。我正在使用Linux Mint Cinnamon Version 18 64 bit
进行开发。
答案 0 :(得分:1)
很难说不知道你的bootloader.asm
做了什么,但是:
链接顺序必定是错误的;
ld -melf_i386 -Ttext=0x0000 console.o hellworld.o -o hellworld.elf
应该是:
ld -melf_i386 -Ttext=0x0000 hellworld.o console.o -o hellworld.elf
(编辑:我看到你有一个链接器脚本,不需要这个重新安排,但你没有用它来链接)。
我怀疑你的引导加载程序加载了一个扇区和你的填充:
asm(".rept 512;"
"hlt;"
".endr");
...防止来自其他目标文件的代码被加载,因为它将hellword.o
填充到(超过)扇区的大小。
问题与头文件的使用无关,这是因为你有两个编译单元成为单独的对象,并且链接时两者的组合大小大于一个扇区(512字节)。
答案 1 :(得分:1)
头文件根本不是问题。当您重新构建代码并将其拆分为多个对象时,它已经确定了构建方式以及jmp main
如何放入最终内核文件的问题。
如果您希望测试完整的更改集以查看它们是否可以解决您的问题,我已经创建了一个set of files来进行下面讨论的所有调整。
虽然您显示了链接描述文件,但您实际上并未使用它。在您的构建文件中,您有:
ld -melf_i386 -Ttext=0x0000 console.o hellworld.o -o hellworld.elf
应该是:
ld -melf_i386 -Tlink.ld console.o hellworld.o -o hellworld.elf
使用-c
(编译但不链接)与 GCC 时,请不要将link.ld
指定为链接描述文件。当您调用 LD 时,可以在链接时指定链接描述文件。这一行:
gcc console.c -m16 -c -o console.o -nostdlib link.ld -ffreestanding
应该是:
gcc console.c -m16 -c -o console.o -nostdlib -ffreestanding
为了让此链接描述文件在输出内核文件中第一个的位置找到jmp main
,您需要更改:
asm("jmp main");
要:
asm(".pushsection .startup\r\n"
"jmp main\r\n"
".popsection\r\n");
.pushsection
暂时将该部分更改为.startup
,输出指令jmp main
,然后将.popsection
部分恢复为以前的状态。链接器脚本故意在.startup
部分之前放置任何内容。这可确保jmp main
(或您放置的任何其他指令)显示为输出内核文件的第一条指令。 \r\n
可以替换为;
(分号)。如果你有 GCC 生成一个汇编文件,\r\n
会产生更漂亮的输出。
正如现在删除的问题的评论中提到的,您的内核文件超过了单个扇区的大小。如果您没有链接描述文件,则默认链接脚本会将数据部分放在代码之后。您的代码重复了hlt
指令,以便您的内核大于1扇区(512字节),并且您的引导加载程序仅使用Int 13h/AH=2h读取单个扇区。
要纠正此删除:
asm(".rept 512;"
"hlt;"
".endr");
并将其替换为:
asm("cli;"
"hlt;");
您应该注意,随着内核的增长,您需要调整bootloader.asm
中读取的扇区数,以确保将所有内核加载到内存中。
我还建议保留 QEMU ,并且其他虚拟机很高兴您只需生成一个众所周知的磁盘映像大小并将引导加载程序和内核放在其中。而不是:
cat bootloader.bin hellworld.bin >disk.img
使用此:
dd if=/dev/zero of=disk.img bs=1024 count=1440
dd if=bootloader.bin of=disk.img seek=0 conv=notrunc
dd if=hellworld.bin of=disk.img seek=1 conv=notrunc
第一个命令生成一个1440kb的零填充文件。这是1.44MB软盘的确切大小。第二个命令在第一个扇区中插入bootloader.bin
而不截断磁盘文件。第三个命令将内核文件放入从磁盘上第二个扇区开始的磁盘映像中,而不截断磁盘映像。
我提供了稍微改进的linker script。对其进行了修改,以消除链接器可能插入到内核中的一些潜在错误,这些内容不太有用,并且特别标识了一些部分,如.rodata
(只读数据)等。
/*
* link.ld
*/
OUTPUT_FORMAT(elf32-i386)
SECTIONS
{
. = 0x0000;
.text : { *(.startup); *(.text) }
.data : { *(.data); *(.rodata) }
.bss : { *(COMMON); *(.bss) }
/DISCARD/ : {
*(.eh_frame);
*(.comment);
*(.note.gnu.build-id);
}
}
与您的问题无关,但可以删除此代码:
asm("mov $0x1000,%ax;"
"mov %ax,%es;"
"mov %ax,%ds");
你在bootloader.asm中执行此操作,因此使用相同的值再次设置这些段寄存器不会做任何有用的事情。
您可以使用extended assembly template通过注册 EAX ( AX )和 EBX传递所需的值来改进input constraints ( BX )而非编码模板内的移动。您的代码可能看起来像:
void kprintf(const char *string)
{
while (*string)
{
asm("int $0x10"
:
:"a"((0x0e<<8) | *string++), /* AH = 0x0e, AL = char to print */
"b"(0)); /* BH = 0x00 page #
BL = 0x00 unused in text mode */
}
}
<<
是 C bit shift left operator。 0x0e<<8
会将0x0e
左移8位,即0x0e00
。 |
是按位 OR ,它有效地将字符打印在低8位中。然后,组件模板通过input constraint "a"
将该值传递到 EAX 寄存器。