使用外部c代码编译asm引导加载程序

时间:2017-11-12 14:01:11

标签: c gcc assembly x86 nasm

我在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

3 个答案:

答案 0 :(得分:9)

编译&链接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(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

运行时可能看起来像:

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指令之前。如果您希望以这个确切的顺序输出这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

code16几乎无用且不应使用

在上一节中,我们了解到一个带参数的简单函数最终无法正常运行,甚至可能会使 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运行时,它看起来应该类似于:

enter image description here

在大多数情况下,GCC生成需要80386 +

的代码

GCC 使用.code16gcc生成的代码存在许多缺点:

  • ES = DS = CS = SS必须为0
  • 代码必须适合第一个64kb
  • GCC 代码不了解20位段:偏移量寻址。
  • 除了最简单的 C 代码之外,GCC不会生成可以在286/186/8086上运行的代码。它以实模式运行,但它使用的是32位操作数,并且在早于80386的处理器上不能使用寻址。
  • 如果您想访问第一个64kb以上的内存位置,那么在调用 C 代码之前,您需要进入Unreal Mode(big)

如果您想从更现代的 C 编译器生成真正的16位代码,我建议OpenWatcom C

  • 内联汇编功能不如 GCC
  • 强大
  • 内联汇编语法不同,但它比 GCC 的内联汇编更容易使用且更不容易出错。
  • 可以生成将在过时的8086/8088处理器上运行的代码。
  • 理解20位段:偏移实模式寻址,并支持远大指针的概念。
  • 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

答案 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]

  • 编写启动加载程序代码而不是此版本
  • 关于ld,gcc,nasm如何工作的一些解释。