Link object files from header files in real mode when using GCC -m16 option?

557 views Asked by At

I would like to implement header files in my c-code which consists partly of GCC inline assembly code for 16 bit real mode but i seem to have linking problems. This is what my header file console.h looks like:

#ifndef CONSOLE_H
#define CONSOLE_H

extern void kprintf(char*);

#endif

and this is 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");
    }
}

the last one 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");
}

My bootloader is in 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

Now I use the following buildscript 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

and the linkscript link.ld:

/*
*  link.ld
*/
OUTPUT_FORMAT(elf32-i386)
SECTIONS
{
   . = 0x0000;
   .text : { *(.startup); *(.text) }
   .data : { *(.data) }
   .bss  : { *(.bss)  }
}

Unfortunately it isn't working because it doesn't print the expected hello world. I think there must be something wrong with the linking command:

ld -melf_i386 -Ttext=0x0000 console.o hellword.o link.ld -o hellworld.elf`

How do I link header-files in 16-bit mode correctly?

When I write the kprintf function directly in the hellworld.c it is working correctly. I am using Linux Mint Cinnamon Version 18 64 bit for development.

2

There are 2 answers

23
Michael Petch On BEST ANSWER

The header files are not really the issue at all. When you restructured the code and split it into multiple objects it has identified issues with how you build and how jmp main is placed into the final kernel file.

I have created a set of files that make all the adjustments discussed below if you wish to test the complete set of changes to see if they rectify your problems.


Although you show the linker script, you aren't actually using it. In your build file you have:

ld  -melf_i386 -Ttext=0x0000 console.o hellworld.o -o hellworld.elf

It should be:

ld  -melf_i386 -Tlink.ld console.o hellworld.o -o hellworld.elf

When using -c (compiles but doesn't link) with GCC don't specify link.ld as a linker script. The linker script can be specified at link time when you invoke LD. This line:

gcc console.c -m16 -c -o console.o -nostdlib link.ld -ffreestanding

Should be:

gcc console.c -m16 -c -o console.o -nostdlib -ffreestanding

In order for this linker script to locate the jmp main in a place that is first in the output kernel file you need to change:

asm("jmp main");

To:

asm(".pushsection .startup\r\n"
    "jmp main\r\n"
    ".popsection\r\n");

The .pushsection temporarily changes the section to .startup, outputs the instruction jmp main and then restores the section with .popsection to whatever it was before. The linker script deliberately places anything in the .startup section before anything else. This ensures the jmp main (or any other instructions you place there) appear as the very first instructions of the output kernel file. The \r\n can be replaced by ; (semicolon). \r\n makes for prettier output if you ever have GCC generate an assembly file.


As mentioned in the comments of a now deleted question your kernel file exceeds the size of a single sector. When you don't have a linker script, the default one will place the data section after the code. Your code has repeated the hlt instruction so that your kernel is greater than 1 sector (512 bytes) and your bootloader only reads a single sector with Int 13h/AH=2h .

To rectify this remove:

asm(".rept 512;"
    "hlt;"
    ".endr");

And replace it with:

asm("cli;"
    "hlt;");

You should be mindful that as your kernel grows you'll need to adjust the number of sectors read in bootloader.asm to ensure all of the kernel is loaded into memory.

I also suggest that to keep QEMU, and other virtual machines happy that you simply generate a well known disk image size and place the bootloader and kernel inside it. Rather than:

cat bootloader.bin hellworld.bin >disk.img

Use this:

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

The first command makes a zero filled file of 1440kb. This is the exact size of a 1.44MB floppy. The second command inserts bootloader.bin in the first sector without truncating the disk file. The third command places the kernel file into the disk images starting at the second sector on the disk without truncating the disk image.


I had made available a slightly improved linker script. It was amended to remove some of the potential cruft that the linker may insert into the kernel that won't be of much use and specifically identifies some of the sections like .rodata (read only data) etc.

/*
*  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);
    }
}

Other Comments

Not related to your question but this code can be removed:

asm("mov $0x1000,%ax;"
    "mov %ax,%es;"
    "mov %ax,%ds");

You do this in bootloader.asm, so setting these segment registers again with the same value won't do anything useful.


You can improve the extended assembly template by using input constraints to pass the values you need via register EAX(AX) and EBX(BX) rather than coding the moves inside the template. Your code could have looked like:

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 */
    }
}

<< is the C bit shift left operator. 0x0e<<8 would shift 0x0e left 8 bits which would be 0x0e00. | is bitwise OR which effectively places the character to print in the lower 8 bits. That value is then passed into the EAX register by the assembly template via input constraint "a".

2
davmac On

It is hard to say without knowing what your bootloader.asm does, but:

  1. The link order must be wrong;

    ld -melf_i386 -Ttext=0x0000 console.o hellworld.o -o hellworld.elf

    should be:

    ld -melf_i386 -Ttext=0x0000 hellworld.o console.o -o hellworld.elf

    (Edit: I see that you have a linker script which would remove the need for this re-arrangement, but you're not using it for the link).

  2. I suspect that your bootloader loads a single sector, and your padding:

    asm(".rept 512;"
        "hlt;"
        ".endr");
    

    ... prevents the code from the other object file from ever being loaded, since it pads hellword.o to (more than) the size of a sector.

The problem is nothing to do with the use of header files, it is because you have two compilation units which become separate objects, and the combined size of both when linked is larger than a sector (512 bytes).