我在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和call print_str
,则在VMWare中启动asm脚本。
我尝试编译:
nasm -f elf32
但是我无法调用org 0x7C00
答案 0 :(得分:9)
这个问题的答案比人们想象的要复杂得多,尽管有可能。引导加载程序的第一阶段(原始地址为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(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
代码的期望。
如果我们在问题中采用您的示例代码:
__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__
,因为它们已经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
函数的示例,该函数采用以空字符结尾的字符串,并使用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 。
在上一节中,我们了解到一个带参数的简单函数最终无法正常运行,甚至可能会使 QEMU 等模拟器崩溃。主要问题是__asm__(".code16\n");
语句对 GCC 生成的代码效果不佳。 Binutils AS documentation说:
'。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 <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 -T link.ld kmain.o hw.o -o kernel.elf
objcopy -O binary kernel.elf kernel.bin
使用qemu-system-i386 -fda kernel.bin
运行时,它看起来应该类似于:
GCC 使用.code16gcc
生成的代码存在许多缺点:
如果您想从更现代的 C 编译器生成真正的16位代码,我建议OpenWatcom C
wlink
Watcom链接器可以生成可用作引导加载程序的基本平面二进制文件。 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
答案 1 :(得分:2)
我在asm中编写了一个启动加载程序,并希望在我的项目中添加一些已编译的C代码。
然后您需要使用16位x86编译器,例如OpenWatcom。
GCC无法安全地构建实模式代码,因为它不知道平台的一些重要功能,包括内存分段。插入.code16
指令将使编译器生成错误的输出。尽管出现在许多教程中,但这条建议完全不正确,不应该使用。
答案 2 :(得分:-3)
首先,我想表达如何将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 <stdio.h>
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 <stdio.h> 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] 强>