Search code examples
assemblyx86bootloaderattosdev

Can't print from kernel.s even though its loaded onto Memory


I am attempting to load the kernel into memory by writing my own boot loader. I've been able to successfully load the kernel into memory - I know that because I used the bochs debugger set the breakpoint to 0x7c00 and stepped through and the system does jump into the kernel. The problem is that after jumping into the kernel none of the print statements (in kernel.s) work. That indicates on the terminal that the kernel has been loaded into memory.

Here is the bootblock.s file (majority of the relevant code resides at label booter:

# bootblock.s
# Empty boot block file

# .equ symbol, expression
# These directive set the value of the symbol to the expression
    .equ    BOOT_SEGMENT,0x07c0
    .equ    DISPLAY_SEGMENT,0xb800
    .equ    KERNEL_LOCATION, 0x1000
    .equ    STACK_SP, 0xffff
    .equ    STACK_SS, 0x0

.text               # Code segment
.globl    _start    # The entry point must be global
.code16             # Real mode


_start:
    ###MAKE BOOTABLE###
    #. = _start + 510
    #.byte = 0x55
    #.byte = 0xaa
    jmp booter

os_size:
    #Place where createimage writes the OS size
    .word 0
    .word 0

print:
  movw  $BOOT_SEGMENT,%ax
  movw  %ax,%ds

print_loop:
  lodsb
  cmpb  $0,%al
  je  print_done
  movb  $14,%ah
  movl  $0x0002,%ebx
  int  $0x10
  jmp  print_loop
print_done:
  retw


booter:

    ###SET UP STACK###
    #Allocating the stack
    movw $STACK_SS, %ax
    movw %ax, %ss
    movw $STACK_SP, %sp

    movl $allocating, %esi
    call print

    movl $done, %esi
    call print

    #Resetting the disk drive, setting %dl and calling int 0x13
    #movb $0x0, %ah
    #movb $0x0, %dl
    #int $0x13

    movl $bootblock_test, %esi
    call print
    movl $hellostring, %esi
    call print

    ###LOAD KERNEL###
    movl $loadingkernel, %esi
    call print

    #Number of sectors to read
    #movb $0x24, %al
    #movb $0x80, %al
    movb $0x08, %al


    movb $0x02, %ah
    #track number
    #movb $0x00, %ch

    #which sector to read from (sector 2 where kernel should be)
    movb $0x02, %cl

    #set up head number
    movb $0x0, %dh

    #Set the drive number to 0x0 (floppy)
    movb $0x0, %dl

    #Time to set es:bx to read from the correct place (0:1000)
    movw $0x0100, %bx
    movw %bx, %es
    movw $0x0, %bx
    #movw $0x0, %ax

    #Setting %ah = 2 and calling int 0x13 (read sector)

    int $0x13

    movl $done, %esi
    call print

    #Booting up at 0x07c0
    #movw $BOOT_SEGMENT, %ax
    #movw %ax, %ds
    #movl $bootmessage, %esi
    #call print


    #%dh/%ch control head numbers, setting them to 0
    #movb $0x0, %dh
    #movb $0x0, %ch

    #movw %ds, 

    ###INVOKE KERNEL###

    #Kernel jump
    movl $readymessage, %esi
    call print

    #Setting %ds = 0x7c0
    movw $0x0100, %ax
    movw %ax, %ds

    #Time to set es:bx to read from the correct place (0:1000)
    movw $0x0100, %bx
    movw %bx, %es
    movw $0x0, %bx


    movl $0x1000, %ax
    jmp %ax
    mov $0x0, %ax

    #If any errors, message will be displayed here
    movl $errormessage, %esi
    call print


forever:
    jmp forever


#Error handling
error:
    movl $errormessage, %esi
    call print

# messages
mystring:  
  .asciz  "test.\n\r"
bootblock_test:
  .asciz "\nBootblock Test\n\r"
hellostring:  
  .asciz  "How are you today?\n\r"
myname:
.asciz "Welcome\n\r"
loadingkernel:
.asciz "Loading Kernel...\n\r"
done:
.asciz "Done!\n\r"
bootmessage:
    .asciz "Booting up...\n\r"
readymessage:
    .asciz "Sliding into yo Kernel like... \n\r"
errormessage:
    .asciz "Something went terribly wrong...\n\r"
rebootmessage:
    .asciz "Press any key to reboot the OS!\n\r"
allocating:
.asciz "Allocating Stack...\n\r"

Here is the kernel.s file:

.data                               # Data segment

# Some strings 
kernel:
    .asciz  "[Kernel]-> "
testing:
    .asciz  "Running a trivial test... "
works:
    .asciz  "Seems Ok. Now go get some sleep :)."
not:
    .asciz  "*Failed*"

# 'Newline' string ('carriage return', 'linefeed', '\0')
newline:
    .byte 10
    .byte 13
    .byte 0

# An integer
result:
    .word 1000



.text                               # Code segment
.code16                             # Real mode
.globl _start                       # The entry point must be global

#
# The first instruction to execute in a program is called the entry
# point. The linker expects to find the entry point in the "symbol" _start
# (with underscore).
#
_start:
    pushw   %bp     # Setup stack frame
    movw    %sp,%bp

    pushw   $newline
    call    displayString   # Print messages
    pushw   $kernel
    call    displayString
    pushw   $testing
    call    displayString
    pushw   $1000
    call    trivialTest # trivialTest(1000)
    addw    $8,%sp      # Pop newline, kernel, testing, and '1000'
    cmpw    %ax,result      
    jne .L6     # If (trivialTest(1000) != 1000) goto L6
    pushw   $works          
    jmp .L12            
.L6:                # Test failed
    pushw   $not            
.L12:
    call    displayString   # Print ok/failed message
    addw    $2,%sp
    pushw   $newline
    call    displayString
    addw    $2,%sp
.L8:                # Loop forever
    jmp .L8

#
# int trivialTest(n)
# {
#     if (n > 0) {
#         trivialTest(n-1);
#     }
#     return n; 
# }

trivialTest:    
    pushw   %bp     # Setup stack frame
    movw    %sp,%bp
    movw    4(%bp),%ax  # Move argument to ax
    testw   %ax,%ax     # Logical compare (sets SF, ZF and PF)
    jg  .L2     # if (argument > 0) goto L2
    xorw    %ax,%ax     # else return 0
    popw    %bp         
    retw                
.L2:
    decw    %ax
    pushw   %ax
    call    trivialTest # trivialTest(argument - 1)
                # (Recursive calls until argument == 0)
    addw    $2,%sp      # Pop argument
    incw    %ax
    popw    %bp
    retw            # Return (argument in ax)

displayString:
    pushw   %bp     # Setup stack frame
    movw    %sp,%bp
    pushw   %ax     # Save ax, bx, cx, si, es
    pushw   %bx
    pushw   %cx
    pushw   %si
    pushw   %es
    movw    %ds, %ax    # Make sure ES points to the right
    movw    %ax, %es    #  segment
    movw    4(%bp),%cx  # Move string adr to cx
    movw    %cx, %si
loop:       
    lodsb           # Load character to write (c) into al,
                #  and increment si
    cmpb    $0, %al     
    jz  done        # if (c == '\0') exit loop
    movb    $14,%ah     # else print c
    movw    $0x0002,%bx
    # int 0x10 sends a character to the display
    # ah = 0xe (14)
    # al = character to write
    # bh = active page number (we use 0x00)
    # bl = foreground color (we use 0x02)
    int $0x10           
    jmp loop
done:
    popw    %es     # Restore saved registers
    popw    %si
    popw    %cx
    popw    %bx
    popw    %ax
    popw    %bp
    retw            # Return to caller

Once again, I've checked in the debugger that the kernel is being loaded into memory (0x1000). I believe the problem is with how I am setting/using certain registers in bootblock.s (mainly : ds,ax ) but I am not exactly sure what it is.


Solution

  • There are a number of issues with the code. Most of the fundamental ones that seem to apply to most 16-bit OS boot loaders can be found in my recent Stackoverflow answer (similar type of question). It covers things you need to be aware of with SS/SP/ES/DS/CS registers, stack performance issues on 8086/8088, some things to look out for on old buggy 8086/8088.

    One specific issue with your code though - If you will be running your code on an 8086/8088 system or emulator (not 286, 386 etc) then you should stick to the 16 bit registers since the 32 bit registers aren't available. Your code uses ESI and EBX registers (32-bit). You should use SI and BX.

    The primary problem in this code is that for the most part the kernel was being read from the disk okay, but it happened to read fewer sectors than the kernel image actually took. This just happened to lead to the variables that were meant to be printed by the kernel to not be loaded.

    I had originally assumed that the kernel appeared so small and reading 8 sectors from the disk was more than enough for the sample code presented:

    #Number of sectors to read
    movb $0x08, %al
    

    I discovered that the original poster was doing an assignment and that someone had posted some crucial information to resolve the problem. I have forked that git project for reference purposes. Some key pieces of information were the type of Makefile being used (with slight differences) and a program called createimage that is provided for the assignment.

    The Makefile is close enough to something like:

    # Makefile for the OS projects.
    # Best viewed with tabs set to 4 spaces.
    
    CC = gcc -Wall -Wextra -std=c99 -g
    LD = ld
    
    # Where to locate the kernel in memory
    KERNEL_ADDR = 0x1000
    
    # Compiler flags
    #-fno-builtin:          Don't recognize builtin functions that do not begin with
    #                       '__builtin_' as prefix.
    #
    #-fomit-frame-pointer:  Don't keep the frame pointer in a register for 
    #                       functions that don't need one.
    #
    #-make-program-do-what-i-want-it-to-do:
    #                       Turn on all friendly compiler flags.
    #
    #-O2:                   Turn on all optional optimizations except for loop unrolling
    #                       and function inlining.
    #
    #-c:                    Compile or assemble the source files, but do not link.
    #
    #-Wall:                 All of the `-W' options combined (all warnings on)
    
    CCOPTS = -c -fomit-frame-pointer -O2 -fno-builtin
    
    # Linker flags
    #-nostartfiles:         Do not use the standard system startup files when linking.
    #
    #-nostdlib:             Don't use the standard system libraries and startup files when
    #                       linking. Only the files you specify will be passed to the linker.
    #          
    #-Ttext X:              Use X as the starting address for the text segment of the output 
    #                       file.
    
    LDOPTS = -nostartfiles -nostdlib -Ttext
    
    # Makefile targets
    all: bootblock createimage kernel image boch_image
    
    kernel: kernel.o
        $(LD) $(LDOPTS) $(KERNEL_ADDR) -o kernel $<
    
    bootblock: bootblock.o
        $(LD) $(LDOPTS) 0x0 -o bootblock $<
    
    createimage: createimage.o
        $(CC) -o createimage $<
    
    # Create an image to put on the floppy
    image: bootblock createimage kernel
        ./createimage ./bootblock ./kernel
    
    # Put the image on the floppy (these two stages are independent, as both
    # vmware and bochs can run using only the image file stored on the harddisk)
    #boot: image
    #   cat ./image > /dev/sda
    
    #write image to boch disk image
    boch_image: image
        dd if=image of=bochs.img conv=notrunc
    
    # Clean up!
    clean:
        rm -f *.o
        rm -f createimage image bootblock kernel 
    
    # No, really, clean up!
    distclean: clean
        rm -f *~
        rm -f \#*
        rm -f *.bak
        rm -f bochsout.txt
    
    # How to compile a C file
    %.o:%.c
        $(CC) $(CCOPTS) $<
    
    # How to assemble
    %.o:%.s
        $(CC) $(CCOPTS) $<
    
    # How to produce assembler input from a C file
    %.s:%.c
        $(CC) $(CCOPTS) -S $<
    

    According to a followup comment by the original poster they are building elf_i386 binaries (Linux ELF format for 32-bit). The above Makefile suggests that the Kernel is being built as an ELF image and then placed on the disk. The ELF format adds a fair amount of padding for each section, so the assumptions that reading 8 sectors from the disk might not be enough. There is no reference to objcopy in the Makefile or converting to a flat binary format. I discovered that the createimage program that they are given extracts the Kernel image from the 32-Bit ELF format and produces output that says how many sectors the bootloader needs to read to get the entire kernel. createimage code is:

    /* createimage.c -- create a bootable image in 16 real mode from several elf file
     */
    #include <stdio.h>
    #include <stdlib.h>
    #include "createimage.h"
    
    int file_process(FILE *elf_file, FILE *image, char *elf_filename);
    long byte_get (unsigned char *field, int size);
    
    int main (int argc, char ** argv)
    {
        //here hasn't check the magic numbers of elf
        if (argc != 3) {
            printf("USAGE:%s bootblock kernel\n", argv[0]);
            return -1;
        } 
        FILE *bootblock, *kernel, *image;
        if ((bootblock = fopen (argv[1], "rb")) == NULL) {
            printf("can't open %s\n", argv[1]);
            return -1;
        }
        if ((image = fopen ("image", "wb")) == NULL) {
            printf("can't open image!\n");
            return -1;
        }
        if (file_process(bootblock, image, argv[1])) {
            printf("process bootblock failed\n");
            return -1;
        }
    
        if ((kernel = fopen (argv[2], "rb")) == NULL) {
            printf("can't open %s\n", argv[2]);
            return -1;
        }
        if (file_process(kernel, image, argv[2])) {
            printf("process kernel failed\n");
            return -1;
        }
    
        fclose(bootblock);
        fclose(kernel);
        fclose(image);
    
        return 0;
    }
    
    long byte_get (unsigned char *field, int size)
    {
        switch (size)
        {
            case 1:
                return *field;
    
            case 2:
                return  ((unsigned int) (field[0])) | (((unsigned int) (field[1])) << 8);
            case 4:
                return  ((unsigned long) (field[0]))
                    |    (((unsigned long) (field[1])) << 8)
                    |    (((unsigned long) (field[2])) << 16)
                    |    (((unsigned long) (field[3])) << 24);
            default:
                printf("byte_get error\n");
                return -1;
        }
    }
    
    
    /* read information from elf file, and write LOAD segment to image file 
     *
     * note: the structure in file is not aligned, we just read it from file byte
     * by byte
     */
    int file_process(FILE *elf_file, FILE *image, char *elf_filename)
    {
        unsigned int header_sz, pheader_sz;
        unsigned long phoff;
        unsigned int p_offset;
        unsigned int p_filesz;
        unsigned int p_memsz;
        elf_header header;
        elf_program_header pheader;
    
        header_sz = sizeof (elf_header);
        pheader_sz = sizeof(elf_program_header);
    
        printf("processing %s:\n", elf_filename);
        printf("header size is: %d\n", header_sz);
        printf("program header size is: %d\n", pheader_sz);
    
        if (header_sz != fread(&header, 1, header_sz, elf_file)) {
            printf("read error!\n");
            return -1;
        }
    
        //get program header's offset
        phoff = byte_get(header.e_phoff, sizeof(header.e_phoff));
    
        printf("Program header table offset in file is :\t %u\n", phoff);
    
        if (fseek(elf_file, phoff, SEEK_SET)) {
            printf("fseek %s failed! at line %d\n", elf_filename, __LINE__);
            return -1;
        }
        //printf("the current position: %d\n", ftell(elf_file));
    
        if (pheader_sz != fread(&pheader, 1, pheader_sz, elf_file)) {
            printf("read error at line %d!\n", __LINE__);
            return -1;
        }
        //get the LOAD segment's offset, filesz, mensz
        p_offset = byte_get(pheader.p_offset, sizeof(pheader.p_offset));
        p_filesz = byte_get(pheader.p_filesz, sizeof(pheader.p_filesz));
        p_memsz = byte_get(pheader.p_memsz, sizeof(pheader.p_memsz));
        printf("p_offset: 0x%x\tp_filesz: 0x%x\tp_memsz: 0x%x\t\n", p_offset, p_filesz, p_memsz);
        //write elf's LOAD segment to image, and pad to 512 bytes(1 sector)
        char *buffer;
        const unsigned int sector_sz = 512;
        const char MBR_signature[] = {0x55, 0xaa};
        unsigned int n_sector;
        unsigned int n_byte;
    
        if (p_memsz % sector_sz != 0)
            n_sector = p_memsz / sector_sz + 1;
        else
            n_sector = p_memsz / sector_sz;
    
        n_byte = n_sector * sector_sz;
    
        if (!(buffer = (char *)calloc(n_byte, sizeof(char)))) {
            printf("malloc buffer failed! at line %d\n", __LINE__);
            return -1;
        }
        if (fseek(elf_file, p_offset, SEEK_SET)) {
            printf("fseek %s failed! at line %d\n", elf_filename, __LINE__);
            return -1;
        }
        if (p_filesz != fread(buffer, 1, p_filesz, elf_file)) {
            printf("read error at line %d!\n", __LINE__);
            return -1;
        }
        if (n_byte != fwrite(buffer, 1, n_byte, image)) {
            printf("write error at line %d!\n", __LINE__);
            return -1;
        }
        //write MBR signature to image, which is 2 bytes
        if (fseek(image, 510, SEEK_SET)) {
            printf("fseek %s failed! at line %d\n", elf_filename, __LINE__);
            return -1;
        }
        if (2 != fwrite(MBR_signature, 1, 2, image)) {
            printf("write error at line %d!\n", __LINE__);
            return -1;
        }
    
        printf("write image:\n%d sectors,\t%d bytes\n", n_sector, n_byte);
    
        return 0;
    }
    

    What is important is that after the code and data sections are extracted (to be put in a disk image with dd) it provides output similar to this at the bottom:

    processing ./kernel:
    header size is: 52
    program header size is: 32
    Program header table offset in file is :         52
    p_offset: 0x54  p_filesz: 0x10db        p_memsz: 0x10db
    write image:
    9 sectors,      4608 bytes
    dd if=image of=bochs.img conv=notrunc
    

    Important information here 9 sectors, 4608 bytes. This says how big the kernel is in sectors. The original poster's code must make sure that they read that many sectors when loading the kernel. So the trivial fix is to change the code to:

    #Number of sectors to read should match output from createimage
    movb $0x09, %al