为什么将char传递给函数会改变它在c中的值?
我目前正在关注构建操作系统的这本工作簿 。
我的目的是编写一个64位内核。 在文本模式下,我已经加载了“内核”代码并将单个字符写入帧缓冲区。
当我通过将代码包装在函数中来添加一个间接级别来将单个字符写入帧缓冲区时,我的问题出现了。 看来传递给函数的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 $2, %eax addl %edx, %eax sall $4, %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 $32, %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 $753666, %edi call write_memory movzbl -24(%rbp), %eax movl -4(%rbp), %edx leal 1(%rdx), %ecx movl %eax, %edx movl %ecx, %esi movl $753666, %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 $16, %rsp movl $1, %esi movl $0, %edi call frame_buffer_offset movl %eax, -4(%rbp) movl -4(%rbp), %eax movl $65, %edx movl %eax, %esi movl $753666, %edi call write_memory movl -4(%rbp), %eax addl $1, %eax movl $7, %edx movl %eax, %esi movl $753666, %edi call write_memory movl $0, %ecx movl $1, %edx movl $7, %esi movl $66, %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);
。 当你说:这符合你所描述的你想要做的事情:
我拍了一张我得到的输出的屏幕截图。 我希望’A’出现在第1行的第0列,’B’出现在第0行的第1列。
我知道你可能在这个问题上发布了错误的代码。 这是我得到的输出:
看来你是OS开发的新手。 您的引导加载程序代码仅将CPU置于32位保护模式,但要运行64位内核,您需要使用64位长模式。 如果你刚刚开始,我建议回到编写一个32位内核,以便在这个早期阶段学习。 在底部,我有一个64位长模式部分,其中包含一个指向longmode教程的链接,该教程可用于修改引导加载程序以运行64位代码。
导致exception行为的首要问题
您遇到的问题主要与您使用GCC生成64位代码但根据引导加载程序代码以32位保护模式运行它有关。 在32位保护模式下运行的64位代码生成可能似乎执行,但它会错误地执行。 在简单的操作系统中,您只需显示video显示,您可能经常会看到意外的输出作为副作用。 你的程序可能会使机器出现三重故障,但你不幸的是副作用似乎在video显示器上显示出来。 你可能错误地认为事情在他们真的没有的时候正常运作。
这个问题有点类似于另一个Stackoverflow问题。 在该问题的原始海报提供了一个完整的例子后,很明显这是他的问题。 我对他解决问题的部分答案如下:
未定义行为的可能原因
在EDIT 2中提供了所有代码和make文件后,很明显一个重要问题是大多数代码都被编译并链接到64位对象和可执行文件。 该代码在32位保护模式下不起作用。
在make文件中进行以下调整:
- 使用GCC进行编译时,需要添加
-m32
选项- 与GNU Assembler ( as )一起使用32位对象进行组装时,需要使用
--32
- 与LD链接时,需要添加
-melf_i386
选项- 使用针对32位对象的NASM进行组装时,需要将
-f elf64
更改为-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作为你的video内存的地址。 它应该是0xb8000。 你需要修改:
#define FRAME_BUFFER_ADDRESS 0xb8002
成为:
#define FRAME_BUFFER_ADDRESS 0xb8000
进行所有这些更改应解决您的问题。 这就是我在上面提到的更改之后看到的输出:
其他观察
在write_memory
你使用:
unsigned char * memory = (unsigned char *) address;
由于您使用的是映射到video显示器的内存映射的0xb8000,因此您应该将其标记为volatile
因为编译器可以优化内容而不知道写入该内存会产生副作用(即在显示器上显示字符)。 您可能希望使用:
volatile unsigned char * memory = (unsigned char *) address;
在你的bootloader.asm
你真的应该明确设置A20线。 您可以在此OSDev Wiki文章中找到有关执行此操作的信息 。 引导加载程序开始执行时A20线的状态可能因仿真器而异。 如果您尝试访问奇数兆字节边界上的内存区域(如0x100000到0x1fffff,0x300000到0x1fffff等),则无法将其设置为可能会导致问题。 对奇数兆字节存储区的访问实际上将从正下方的偶数存储区读取数据。 这通常不是您想要的行为。
64位长模式
如果要运行64位代码,则需要将处理器置于64位长模式。 这比进入32位保护模式要多一些。 有关64位长模的信息可以在OSDev wiki中找到。 一旦正确使用64位长模式,您就可以使用GCC生成的64位指令。