为什么将 char 传递给函数会改变它在 c 中的值?

Why does passing a char to a function change it's value in c?

我目前正在关注 this workbook 构建操作系统。

我打算写一个64位内核。在文本模式下,我已经加载了 "kernel" 代码并将单个字符写入帧缓冲区。

当我通过将代码包装在一个函数中来添加一个间接级别以将单个字符写入帧缓冲区时,我的问题就出现了。传递给函数的 char 值似乎以某种方式被破坏了。

我有三个文件:

bootloader.asm

; bootloader.asm
[org 0x7c00]
KERNEL_OFFSET equ 0x1000

mov bp, 0x9000
mov sp, bp

; load the kernel from boot disk
mov bx, KERNEL_OFFSET
mov dl, dl ; boot drive is set to dl
mov ah, 0x02 ; bios read sector
mov al, 15 ; read 15 sectors    
mov ch, 0x00 ; cylinder 0
mov cl, 0x02 ; read from 2nd sector
mov dh, 0x00 ; select head 0
int 0x13

; THERE COULD BE ERRORS HERE BUT FOR NOW ASSUME IT WORKS

; switch to protected mode
cli

lgdt [gdt.descriptor]

mov eax, cr0
or eax, 1
mov cr0, eax
jmp CODE_SEGMENT:start_protected_mode

[bits 32]
start_protected_mode:
    mov ax, DATA_SEGMENT
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov ebp, 0x90000
    mov esp, ebp

    call KERNEL_OFFSET
    jmp $

[bits 16]
gdt: ; Super Simple Global Descriptor Table
.start:
.null:
    dd 0x0
    dd 0x0
.code:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10011010b
    db 11001111b
    db 0x0
.data:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10010010b
    db 11001111b
    db 0x0
.end:
.descriptor:
    dw .end - .start
    dd .start

CODE_SEGMENT equ gdt.code - gdt.start
DATA_SEGMENT equ gdt.data - gdt.start

times 510-($-$$) db 0
dw 0xaa55

bootkernel.asm

[bits 32]
[extern main]
[global _start]
_start:
    call main
    jmp $

kernel.c

// LEGACY MODE VIDEO DRIVER
#define FRAME_BUFFER_ADDRESS 0xb8002
#define GREY_ON_BLACK 0x07
#define WHITE_ON_BLACK 0x0f

void write_memory(unsigned long address, unsigned int index, unsigned char value)
{
    unsigned char * memory = (unsigned char *) address;
    memory[index] = value;
}

unsigned int frame_buffer_offset(unsigned int col, unsigned int row)
{
    return 2 * ((row * 80u) + col);
}

void write_frame_buffer_cell(unsigned char c, unsigned char a, unsigned int col, unsigned int row)
{
    unsigned int offset = frame_buffer_offset(col, row);
    write_memory(FRAME_BUFFER_ADDRESS, offset, c);
    write_memory(FRAME_BUFFER_ADDRESS, offset + 1, a);
}

void main()
{
    unsigned int offset = frame_buffer_offset(0, 1);
    write_memory(FRAME_BUFFER_ADDRESS, offset, 'A');
    write_memory(FRAME_BUFFER_ADDRESS, offset + 1, GREY_ON_BLACK);

    write_frame_buffer_cell('B', GREY_ON_BLACK, 0, 1);
}

.text 部分链接到从 0x1000 开始,这是引导加载程序期望内核启动的位置。

linker.ld脚本是

SECTIONS
{
    . = 0x1000;

    .text : { *(.text) } /* Kernel is expected at 0x1000 */
}

将所有这些放在一起的 Make 文件是:

bootloader.bin: bootloader.asm
    nasm -f bin bootloader.asm -o bootloader.bin

bootkernel.o: bootkernel.asm
    nasm -f elf64 bootkernel.asm -o bootkernel.o

kernel.o: kernel.c
    gcc-6 -Wextra -Wall -ffreestanding -c kernel.c -o kernel.o

kernel.bin: bootkernel.o kernel.o linker.ld
    ld -o kernel.bin -T linker.ld bootkernel.o kernel.o --oformat binary

os-image: bootloader.bin kernel.bin
    cat bootloader.bin kernel.bin > os-image

qemu: os-image
    qemu-system-x86_64 -d guest_errors -fda os-image -boot a

我已经截屏了我得到的输出。我希望 'A' 出现在第 1 行的第 0 列,'B' 出现在第 0 行的第 1 列。出于某种原因,我得到了另一个角色。

gcc-6 -S 的输出kernel.c

    .file   "kernel.c"
    .text
    .globl  write_memory
    .type   write_memory, @function
write_memory:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -24(%rbp)
    movl    %esi, -28(%rbp)
    movl    %edx, %eax
    movb    %al, -32(%rbp)
    movq    -24(%rbp), %rax
    movq    %rax, -8(%rbp)
    movl    -28(%rbp), %edx
    movq    -8(%rbp), %rax
    addq    %rax, %rdx
    movzbl  -32(%rbp), %eax
    movb    %al, (%rdx)
    nop
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   write_memory, .-write_memory
    .globl  frame_buffer_offset
    .type   frame_buffer_offset, @function
frame_buffer_offset:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -8(%rbp), %edx
    movl    %edx, %eax
    sall    , %eax
    addl    %edx, %eax
    sall    , %eax
    movl    %eax, %edx
    movl    -4(%rbp), %eax
    addl    %edx, %eax
    addl    %eax, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   frame_buffer_offset, .-frame_buffer_offset
    .globl  write_frame_buffer_cell
    .type   write_frame_buffer_cell, @function
write_frame_buffer_cell:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    , %rsp
    movl    %esi, %eax
    movl    %edx, -28(%rbp)
    movl    %ecx, -32(%rbp)
    movb    %dil, -20(%rbp)
    movb    %al, -24(%rbp)
    movl    -32(%rbp), %edx
    movl    -28(%rbp), %eax
    movl    %edx, %esi
    movl    %eax, %edi
    call    frame_buffer_offset
    movl    %eax, -4(%rbp)
    movzbl  -20(%rbp), %edx
    movl    -4(%rbp), %eax
    movl    %eax, %esi
    movl    3666, %edi
    call    write_memory
    movzbl  -24(%rbp), %eax
    movl    -4(%rbp), %edx
    leal    1(%rdx), %ecx
    movl    %eax, %edx
    movl    %ecx, %esi
    movl    3666, %edi
    call    write_memory
    nop
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   write_frame_buffer_cell, .-write_frame_buffer_cell
    .globl  main
    .type   main, @function
main:
.LFB3:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    , %rsp
    movl    , %esi
    movl    [=15=], %edi
    call    frame_buffer_offset
    movl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    movl    , %edx
    movl    %eax, %esi
    movl    3666, %edi
    call    write_memory
    movl    -4(%rbp), %eax
    addl    , %eax
    movl    , %edx
    movl    %eax, %esi
    movl    3666, %edi
    call    write_memory
    movl    [=15=], %ecx
    movl    , %edx
    movl    , %esi
    movl    , %edi
    call    write_frame_buffer_cell
    nop
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE3:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 6.2.0-3ubuntu11~16.04) 6.2.0 20160901"
    .section    .note.GNU-stack,"",@progbits

如果将代码修改为:

,我可以重现您的确切输出
unsigned int offset = frame_buffer_offset(0, 1);
write_memory(FRAME_BUFFER_ADDRESS, offset, 'A');
write_memory(FRAME_BUFFER_ADDRESS, offset + 1, GREY_ON_BLACK);

write_frame_buffer_cell('B', GREY_ON_BLACK, 1, 0);

不同之处在于最后一行 ('B', GREY_ON_BLACK, 1, 0);。最初你有 ('B', GREY_ON_BLACK, 0, 1); 。这与你所说的你试图做的是一致的:

I've taken a screen shot of the output that I am getting. I expect 'A' to appear in the 0th column of the 1st row and for 'B' to appear on the 1st column of the 0th row.

我猜你可能在这个问题中发布了错误的代码。这是我得到的输出:


看来您是 OS 开发的新手。您的引导加载程序代码仅将 CPU 置于 32 位保护模式,但对于 运行 64 位内核,您需要处于 64 位长模式。如果您刚刚起步,我建议您回过头来编写一个 32 位内核,以便在这个早期阶段进行学习。在底部,我有一个 64 位长模式 部分,其中有一个 link 长模式教程,可用于将您的引导加载程序修改为 运行 64 位代码。


导致异常行为的主要问题

您遇到的问题主要与以下事实有关:您使用 GCC 生成 64 位代码,但您 运行 将其置于 32 位保护中根据您的引导加载程序代码模式。 64 位代码生成 运行ning 在 32 位保护模式下可能看起来可以执行,但它会执行不正确。在简单的 OSes 中,您只是向视频显示器显示,您可能经常会看到意外的输出作为副作用。你的程序可以使机器出现三重故障,但你很不幸,副作用似乎在视频显示器上显示了一些东西。您可能误以为一切正常,但实际上并非如此。

这个问题有点类似于另一个问题。在该问题的原始发布者提供了一个完整的示例之后,很明显这是他的问题。我对他解决问题的部分回答如下:

Likely Cause of Undefined Behavior

After all the code and the make file were made available in EDIT 2 it became clear that one significant problem was that most of the code was compiled and linked to 64-bit objects and executables. That code won't work in 32-bit protected mode.

In the make file make these adjustments:

  • When compiling with GCC you need to add -m32 option
  • When assembling with GNU Assembler (as) targeting 32-bit objects you need to use --32
  • When linking with LD you need to add the -melf_i386 option
  • When assembling with NASM targeting 32-bit objects you need to change -f elf64 to -f elf32

考虑到这一点,您可以更改 Makefile 以生成 32 位代码。它可能看起来像:

bootloader.bin: bootloader.asm
    nasm -f bin bootloader.asm -o bootloader.bin

bootkernel.o: bootkernel.asm
    nasm -f elf32 bootkernel.asm -o bootkernel.o

kernel.o: kernel.c
    gcc-6 -m32 -Wextra -Wall -ffreestanding -c kernel.c -o kernel.o

kernel.bin: bootkernel.o kernel.o linker.ld
    ld -melf_i386 -o kernel.bin -T linker.ld bootkernel.o kernel.o --oformat binary

os-image: bootloader.bin kernel.bin
    cat bootloader.bin kernel.bin > os-image

qemu: os-image
    qemu-system-x86_64 -d guest_errors -fda os-image -boot a

我了解到,当您的代码开始出现问题时,您最终尝试将 0xb8002 作为视频内存的地址。它应该是 0xb8000。您需要修改:

#define FRAME_BUFFER_ADDRESS 0xb8002

成为:

#define FRAME_BUFFER_ADDRESS 0xb8000

进行所有这些更改应该可以解决您的问题。这是经过上述更改后我得到的输出:


其他观察结果

write_memory 你使用:

unsigned char * memory = (unsigned char *) address;

由于您使用的是内存映射到视频显示器的 0xb8000,因此您应该将其标记为 volatile,因为编译器可以在不知道写入该内存有副作用的情况下进行优化(即在显示器上显示字符)。您可能希望使用:

volatile unsigned char * memory = (unsigned char *) address;

在您的 bootloader.asm 中,您确实应该明确设置 A20 行。您可以在 OSDev Wiki article 中找到有关执行此操作的信息。引导加载程序开始执行时 A20 线的状态可能因仿真器而异。如果您尝试访问奇数兆字节边界上的内存区域(如 0x100000 到 0x1fffff、0x300000 到 0x1fffff 等),则未能将其设置为可能会导致问题。对奇数兆字节内存区域的访问实际上将从其正下方的偶数内存区域读取数据。这通常不是您想要的行为。


64 位长模式

如果您想要 运行 64 位代码,您需要将处理器置于 64 位长模式。这比进入 32 位保护模式要复杂一些。有关 64 位长模式的信息可以在 OSDev wiki 中找到。一旦正确进入 64 位长模式,您就可以使用 GCC.

生成的 64 位指令