即使将其加载到内存中,也无法从 kernel.s 打印

Posted

技术标签:

【中文标题】即使将其加载到内存中,也无法从 kernel.s 打印【英文标题】:Can't print from kernel.s even though its loaded onto Memory 【发布时间】:2015-09-20 19:33:50 【问题描述】:

我正在尝试通过编写自己的引导加载程序将内核加载到内存中。我已经能够成功地将内核加载到内存中 - 我知道因为我使用了 bochs 调试器将断点设置为 0x7c00 并单步执行并且系统确实跳转到了内核。问题是,在进入内核后,打印语句(在 kernel.s 中)都不起作用。这在终端上表明内核已加载到内存中。

这是 bootblock.s 文件(大部分相关代码位于标签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"

这是 kernel.s 文件:

.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

我再次在调试器中检查了内核正在加载到内存 (0x1000)。我认为问题在于我如何在 bootblock.s 中设置/使用某些寄存器(主要是:ds,ax),但我不确定它是什么。

【问题讨论】:

除了@user3144770 在答案中给出的内容外,我只能假设在您的链接器脚本或链接器选项中您已正确设置了引导加载程序和内核的起点。 根据代码,引导加载程序的原点将设置为0x0000,因为您在引导加载程序段寄存器中明确使用0x07C0 段。鉴于您打算跳转到内核的外观(您需要在答案中提到的远调用),它会 出现 没有您打算为内核的起点提供的更多信息是0x1000。如果您的链接器命令或链接器脚本未正确设置,那么如果您曾经使用不可重定位代码(当前段内的绝对内存寻址),这可能会导致潜在问题 这是一个小问题,超出了答案中提到的使用 32 位寄存器(如 %esi)的范围。如果您的代码被修改为使用 16 位和 8 位寄存器,并且您要找到一台真正的 8086/8088(不是 80286+ 的问题)机器并运行它,您会发现每次访问都会有性能损失堆栈(4 个时钟周期)。问题?您将 SP(堆栈指针)设置为 -1(0xFFFF)。因为 -1 是一个奇数的内存位置,所以每次 CPU 从堆栈中检索某些内容时,您都会为此付出代价。修复:将 STACK_SP 更改为 0x0000 而不是 0xFFFF 。 如果您问第一次将某些东西压入堆栈时将堆栈设置为零并写入位置 0x00 是否危险?不。当压栈时,SP 减 2 ,然后存储一个字。 0x0000-2=-2=0xFFFE。 -2 是一个偶数内存位置,因此在真正的 8086/8088 处理器上可以避免这种性能损失。 我之前说过it would appear without further information that you intended for the origin point of the kernel to be 0x1000。这是基于您的错误 jmp movl $0x1000, %ax; jmp %ax 的观察结果。我刚刚注意到上面的代码似乎暗示您打算使用 0x0100 作为段,使用 0x0000 作为跳转到的偏移量。这意味着您也希望将内核的原点设置为 0x0000(链接器)(如果这是有意的话) 【参考方案1】:

这些观察结果可能对您有所帮助:

在尝试加载内核时,您应该取消注释设置轨道号的行。

displayString 例程中无需设置 ES 寄存器。只需要一个正确的 DS。

您正试图通过近跳跳到内核! (jmp %ax) 那行不通。你需要一个段间跳跃。使用jmp $0x0000:$0x0100

您应该避免使用 32 位寄存器(例如 %esi)来传递地址,因为这是 16 位代码。

【讨论】:

其他应该考虑的事情是,在内核和引导加载程序代码中,他正在使用lodsb,而在他的引导加载程序(或内核)中,他没有明确地使用 CLD 设置方向标志(CLD 很可能是他对编写的代码的意图)。不能假定方向标志的状态是否已设置,因此明确设置它通常是一种好习惯。 是的,好点。在 Linux 程序的用户模式下,ABI 要求为新启动的程序清除 DF(即在 execve 之后)。这里不是这样。 您不能保证方向标志是由 Bios 设置的。在您的代码中,您使用 lodsb ,它依赖于方向标志来确定是向前还是向后移动。您的lodsb 代码是在假设%di 递增1 的情况下编写的,所以在某个地方(您可以在设置堆栈后立即执行此操作)您需要放置CLD 指令(无操作数)。阅读有关此指令的更多信息here .与此指令相反的是 STD,它使 lodsb,movsb 和相关指令在每次使用后递减 %di/%si @DynamoBooster DUH,愚蠢的错误对不起。在 GNU 汇编器中,段和偏移之间应该是 逗号,而不是 冒号。所以试试jmp $0x0000,$0x1000 @DynamoBooster 忽略我向您建议的最后一件事。我发现似乎是互联网上其他地方的任务。而不是使用objcopy,您的项目中有一个名为 createimage 的程序可以执行类似的操作。我假设你有 createimage?【参考方案2】:

代码存在许多问题。大多数似乎适用于大多数 16 位操作系统引导加载程序的基本问题都可以在我最近的*** answer(类似类型的问题)中找到。它涵盖了使用 SS/SP/ES/DS/CS 寄存器时需要注意的事项、8086/8088 上的堆栈性能问题,以及旧车 8086/8088 上需要注意的一些事项。

您的代码存在一个特定问题 - 如果您将在 8086/8088 系统或仿真器(不是 286、386 等)上运行代码,那么您应该坚持使用 16 位寄存器,因为 32 位寄存器不是可用的。您的代码使用 ESIEBX 寄存器(32 位)。你应该使用 SIBX

此代码中的主要问题是,在大多数情况下,内核都可以从磁盘读取,但它读取的扇区碰巧比内核映像实际占用的扇区少。这恰好导致内核打印的变量没有被加载。

我最初认为内核看起来如此之小,从磁盘读取 8 个扇区对于提供的示例代码来说已经绰绰有余了:

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

我发现原始发帖人正在执行一项任务,并且有人发布了一些重要信息来解决问题。我已经分叉了git project 以供参考。一些关键信息是使用的 Makefile 的类型(略有不同)和为分配提供的名为 createimage 的程序。

Makefile 足够接近类似于:

# 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 $<

根据原始发布者的后续评论,他们正在构建 elf_i386 二进制文件(32 位的 Linux ELF 格式)。上面的 Makefile 表明内核正在构建为 ELF 映像,然后放置在磁盘上。 ELF 格式为每个部分添加了相当数量的填充,因此从磁盘读取 8 个扇区 的假设可能还不够。 Makefile 中没有对objcopy 的引用或转换为平面二进制格式。我发现他们提供的createimage 程序从 32 位 ELF 格式中提取内核映像,并生成输出说明引导加载程序需要读取多少扇区才能获得整个内核。 createimage 代码是:

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

重要的是,在提取代码和数据部分后(要放入带有dd 的磁盘映像中),它会在底部提供与此类似的输出:

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

这里有重要信息9 sectors, 4608 bytes。这说明内核在扇区中有多大。原始发布者的代码必须确保他们在加载内核时读取了那么多扇区。所以简单的解决方法是将代码更改为:

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

【讨论】:

以上是关于即使将其加载到内存中,也无法从 kernel.s 打印的主要内容,如果未能解决你的问题,请参考以下文章

即使值存在,也无法单独从 Redis 加载值

即使使用 JSON.parse,Snowflake 也无法将对象绑定到查询中以将其插入到变体列中

即使使用推荐的设置,也无法从渲染器/预加载脚本中访问电子函数

无法从内存中正确加载位图

AVAudioPlayer 是从磁盘流式传输文件,还是一次将其全部加载到内存中?

即使 php 代码正在运行,也无法从 MySQL 加载我的数据?