用外部c代码编译一个asm引导程序 [英] Compile an asm bootloader with external c code
问题描述
我在asm中编写了一个引导加载程序,并希望在我的项目中添加一些已编译的C代码。
我在这里创建了一个测试函数:
$ b
test.c
__ asm __(。code16 \ N);
$ b $ void print_str(){
__asm__ __volatile __(mov $'A',%al \ n);
__asm__ __volatile __(mov $ 0x0e,%ah\\\
);
__asm__ __volatile __(int $ 0x10\\\
);
}
这里是asm代码(boot loader):
$ b 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
调用print_str;调用函数
mov si,欢迎
调用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
;调用print_string
mov di,cmd_version
调用strcmp
jc .version
jmp mainloop
.version:
mov si,名称
调用print_string
mov al,''
int 10h
mov si,版本
调用print_string
mov si,line_return
调用print_string
jmp mainloop
名字db'MOS',0
version db'v0.1',0
welcome db'由Marius Van Nieuwenhuyse开发', 0x0D,0x0A,0
prompt db'>',0
line_return db 0x0D,0x0A,0
缓冲时间64 db 0
cmd_version db'version' ,0
%includefunctions / print.asm
%includefunctions / getstr.asm
%includefunctions / strcmp.asm
次510 - ($ - $$)db 0
dw 0xaa55
I需要像简单的asm函数一样调用c函数
如果没有extern和调用 print_str
,asm脚本将在VMWare中启动。
我试着编译:
nasm -f elf32
但我无法致电org 0x7C00
编译和链接NASM和GCC代码
这个问题有一个比人们可能认为的更复杂的答案,尽管这是可能的。引导加载程序的第一阶段(在物理地址0x07c00处加载的原始512字节)是否可以调用 C 函数?是的,但它需要重新考虑你如何建立你的项目。
为此,您不能再使用 NASM 为我们 -f bin
。这也意味着你不能使用 org 0x7c00
来告诉汇编程序代码的起始地址。您需要通过链接器(直接使用 LD 或 GCC 进行链接)完成此操作。由于链接器会将内容放在内存中,所以我们不能依赖于在输出文件中放置引导扇区签名 0xaa55
。我们可以让链接器为我们做这件事。
你会发现的第一个问题是由 GCC 内部使用的默认链接器脚本摆脱我们想要的方式。我们需要创建自己的。这样的链接脚本必须将原始点(虚拟内存地址aka VMA)设置为0x7c00,将数据前的程序集文件中的代码放置到文件中的偏移量510处。我不打算写关于链接器脚本的教程。
您会注意到最后一行出现 A
字母。这就是我们期望从 print_str
代码。
GCC内联程序很难获得正确的结果
如果我们在问题中提供示例代码:
__ asm__ __volatile __(mov $'A',%al \ n);
__asm__ __volatile __(mov $ 0x0e,%ah\\\
);
__asm__ __volatile __(int $ 0x10\\\
);
编译器可以自由地对这些 __ asm __
如果它想要的话。 int $ 0x10
可以出现在MOV指令之前。如果你希望这三行按这个顺序输出,你可以将它们合并为一个这样的形式:
__ asm__ __volatile __( mov $'A',%al\\\
\t
mov $ 0x0e,%ah\\\
\t
int $ 0x10);
这些是基本的汇编语句。不需要在它们上面指定 __ volatile __
,因为它们已经是 p>
在大多数情况下,GCC生成需要80386 +的代码+
对于使用 .code16gcc
的 GCC 生成的代码,缺点是很多:
如果您想从更现代的 C 生成真正的16位代码,编译器我推荐 OpenWatcom C
- 内联程序集不如 GCC
- 内联程序集语法不同,但使用更简单并且比 GCC 的内联汇编更容易出错。
- 可以生成可在过时的8086/8088处理器上运行的代码。
- 了解20位段:偏移实型寻址并支持远的和巨大的指针 。
-
wlink
Watcom链接器可以生成可用作引导加载程序的基本平面二进制文件。 $ b
零填充BSS部分
BIOS启动顺序不能保证内存实际上为零。这对零初始化区域 BSS 造成潜在问题。在第一次调用 C 代码之前,应该用我们的汇编代码填充零区域。我最初编写的链接器脚本定义了一个符号 __ bss_start
,它是 BSS 内存的偏移量, __ bss_sizeb
是以字节为单位的大小。使用这些信息,您可以使用 STOSB 指令轻松地填满零。在 hw.asm
的顶部,您可以添加:
extern __bss_sizeb
extern __bss_start
在 CLD 指令之后并且在调用之前任何 C 代码都可以通过这种方式进行零填充:
;零填充BSS部分
mov cx,__bss_sizeb;在链接器脚本中计算的BSS的大小
mov di,__bss_start;在链接器脚本中定义的BSS的开始
rep stosb; AL仍为零,填充内存为零
其他建议
为了减少编译器生成的代码的膨胀,使用 -fomit-frame-pointer
。用 -Os
编译可以优化空间(而不是速度)。我们对于由BIOS加载的初始代码限制了空间(512字节),因此这些优化可能是有益的。用于编译的命令行可以显示为:
gcc -fno-PIC -fomit-frame-pointer -freestanding -m16 -Os -c kmain.c -o kmain.o
I write a boot loader in asm and want to add some compiled C code in my project.
I created a test function here:
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");
}
And here is the asm code (the boot loader):
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
I need to call the c function like a simple asm function
Without the extern and the call print_str
, the asm script boot in VMWare.
I tried to compile with:
nasm -f elf32
But i can't call org 0x7C00
Compiling & Linking NASM and GCC Code
This question has a more complex answer than one might believe, although it is possible. Can the first stage of a bootloader (the original 512 bytes that get loaded at physical address 0x07c00) make a call into a C function? Yes, but it requires rethinking how you build your project.
For this to work you can no longer us -f bin
with NASM. This also means you can't use the org 0x7c00
to tell the assembler what address the code expects to start from. You'll need to do this through a linker (either us LD directly or GCC for linking). Since the linker will lay things out in memory we can't rely on placing the boot sector signature 0xaa55
in our output file. We can get the linker to do that for us.
The first problem you will discover is that the default linker scripts used internally by GCC don't lay things out the way we want. We'll need to create our own. Such a linker script will have to set the origin point (Virtual Memory Address aka VMA) to 0x7c00, place the code from your assembly file before the data and place the boot signature at offset 510 in the file. I'm not going to write a tutorial on Linker scripts. The Binutils Documentation contains almost everything you need to know about linker scripts.
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);
}
}
This script should create an ELF executable that can be converted to a flat binary file with OBJCOPY. We could have output as a binary file directly but I separate the two processes out in the event I want to include debug information in the ELF version for debug purposes.
Now that we have a linker script we must remove the ORG 0x7c00
and the boot signature. For simplicity sake we'll try to get the following code (hw.asm
) to work:
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
You can include all your other code, but this sample will still demonstrate the basics of calling into a C function.
Assume the code above you can now generate the ELF object from hw.asm
producing hw.o
using this command:
nasm -f elf32 hw.asm -o hw.o
You compile each C file with something like:
gcc -ffreestanding -c kmain.c -o kmain.o
I placed the C code you had into a file called kmain.c
. The command above will generate kmain.o
. I noticed you aren't using a cross compiler so you'll want to use -fno-PIE
to ensure we don't generate relocatable code. -ffreestanding
tells GCC the C standard library may not exist, and main
may not be the program entry point. You'd compile each C file in the same way.
To link this code to a final executable and then produce a flat binary file that can be booted we do this:
ld -melf_i386 -T link.ld kmain.o hw.o -o kernel.elf
objcopy -O binary kernel.elf kernel.bin
You specify all the object files to link with the LD command. The LD command above will produce a 32-bit ELF executable called kernel.elf
. This file can be useful in the future for debugging purposes. Here we use OBJCOPY to convert kernel.elf
to a binary file called kernel.bin
. kernel.bin
can be used as a bootloader image.
You should be able to run it with QEMU using this command:
qemu-system-i386 -fda kernel.bin
When run it may look like:
You'll notice the letter A
appears on the last line. This is what we'd expect from the print_str
code.
GCC Inline Assembly is Hard to Get Right
If we take your example code in the question:
__asm__ __volatile__("mov $'A' , %al\n");
__asm__ __volatile__("mov $0x0e, %ah\n");
__asm__ __volatile__("int $0x10\n");
The compiler is free to reorder these __asm__
statements if it wanted to. The int $0x10
could appear before the MOV instructions. If you want these 3 lines to be output in this exact order you can combine them into one like this:
__asm__ __volatile__("mov $'A' , %al\n\t"
"mov $0x0e, %ah\n\t"
"int $0x10");
These are basic assembly statements. It's not required to specify __volatile__
on them as they are already implicitly volatile, so it has no effect. From the original poster's answer it is clear they want to eventually use variables in __asm__
blocks. This is doable with extended inline assembly (the instruction string is followed by a colon :
followed by constraints.):
With extended asm you can read and write C variables from assembler and perform jumps from assembler code to C labels. Extended asm syntax uses colons (‘:’) to delimit the operand parameters after the assembler template:
asm [volatile] ( AssemblerTemplate : OutputOperands [ : InputOperands [ : Clobbers ] ])
This answer isn't a tutorial on inline assembly. The general rule of thumb is that one should not use inline assembly unless you have to. Inline assembly done wrong can create hard to track bugs or have unusual side effects. Unfortunately doing 16-bit interrupts in C pretty much requires it, or you write the entire function in assembly (ie: NASM).
This is an example of a print_chr
function that take a nul terminated string and prints each character out one by one using 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
would be modified to look like this:
push welcome
call print_str ;call function
The idea when this is assembled/compiled (using the commands in the first section of this answer) and run is that it print out the welcome
message. Unfortunately it will almost never work, and may even crash some emulators like QEMU.
code16 is Almost Useless and Should Not be Used
In the last section we learn that a simple function that takes a parameter ends up not working and may even crash an emulator like QEMU. The main problem is that the __asm__(".code16\n");
statement really doesn't work well with the code generated by GCC. The Binutils AS documentation says:
‘.code16gcc’ provides experimental support for generating 16-bit code from gcc, and differs from ‘.code16’ in that ‘call’, ‘ret’, ‘enter’, ‘leave’, ‘push’, ‘pop’, ‘pusha’, ‘popa’, ‘pushf’, and ‘popf’ instructions default to 32-bit size. This is so that the stack pointer is manipulated in the same way over function calls, allowing access to function parameters at the same stack offsets as in 32-bit mode. ‘.code16gcc’ also automatically adds address size prefixes where necessary to use the 32-bit addressing modes that gcc generates.
.code16gcc
is what you really need to be using, not .code16
. This force GNU assembler on the back end to emit address and operand prefixes on certain instructions so that the addresses and operands are treated as 4 bytes wide, and not 2 bytes.
The hand written code in NASM doesn't know it will be calling C instructions, nor does NASM have a directive like .code16gcc
. You'll need to modify the assembly code to push 32-bit values on to the stack in real mode. You will also need to override the call
instruction so that the return address needs to be treated as a 32-bit value, not 16-bit. This code:
push welcome
call print_str ;call function
Should be:
jmp 0x0000:setcs
setcs:
cld
push dword welcome
call dword print_str ;call function
GCC has a requirement that the direction flag be cleared before calling any C function. I added the CLD instruction to the top of the assembly code to make sure this is the case. GCC code also needs to have CS to 0x0000 to work properly. The FAR JMP does just that.
You can also drop the __asm__(".code16gcc\n");
on modern GCC that supports the -m16
option. -m16
automatically places a .code16gcc
into the file that is being compiled.
Since GCC also uses the full 32-bit stack pointer it is a good idea to initialize ESP with 0x7c00, not just SP. Change mov sp, 0x7C00
to mov esp, 0x7C00
. This ensures the full 32-bit stack pointer is 0x7c00.
The modified kmain.c
code should now look like:
#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));
}
}
and 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
These commands can be build the bootloader with:
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
When run with qemu-system-i386 -fda kernel.bin
it should look simialr to:
In Most Cases GCC Produces Code that Requires 80386+
There are number of disadvantages to GCC generated code using .code16gcc
:
- ES=DS=CS=SS must be 0
- Code must fit in the first 64kb
- GCC code has no understanding of 20-bit segment:offset addressing.
- For anything but the most trivial C code, GCC doesn't generate code that can run on a 286/186/8086. It runs in real mode but it uses 32-bit operands and addressing not available on processors earlier than 80386.
- If you want to access memory locations above the first 64kb then you need to be in Unreal Mode(big) before calling into C code.
If you want to produce real 16-bit code from a more modern C compiler I recommend OpenWatcom C
- The inline assembly is not as powerful as GCC
- The inline assembly syntax is different but it is easier to use and less error prone than GCC's inline assembly.
- Can generate code that will run on antiquated 8086/8088 processors.
- Understands 20-bit segment:offset real mode addressing and supports the concept of far and huge pointers.
wlink
the Watcom linker can produce basic flat binary files usable as a bootloader.
Zero Fill the BSS Section
The BIOS boot sequence doesn't guarantee that memory is actually zero. This causes a potential problem for the zero initialized region BSS. Before calling into C code for the first time the region should be zero filled by our assembly code. The linker script I originally wrote defines a symbol __bss_start
that is the offset of the BSS memory and __bss_sizeb
is the size in bytes. Using this info you can use the STOSB instruction to easily zero fill it. At the top of hw.asm
you can add:
extern __bss_sizeb
extern __bss_start
And after the CLD instruction and before calling any C code you can do the zero fill this way:
; 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
Other Suggestions
To reduce the bloat of the code generated by the compiler it can be useful to use -fomit-frame-pointer
. Compiling with -Os
can optimize for space (rather than speed). We have limited space (512 bytes) for the initial code loaded by the BIOS so these optimizations can be beneficial. The command line for compiling could appear as:
gcc -fno-PIC -fomit-frame-pointer -ffreestanding -m16 -Os -c kmain.c -o kmain.o
这篇关于用外部c代码编译一个asm引导程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!