如何在 i386 中正确设置 ss 和 sp 寄存器

How to set ss and sp registers correctly in i386

我目前正在写一个bootloader并且已经开始在我的512B中运行超出space,所以我已经开始在512B之外写更多的代码并打算使用bootloader来读取它进入记忆。我在代码末尾添加了:

stack_start:
    resb 4096
stack_end:

这样我就可以在操作系统代码的末尾为堆栈分配space。目前在我的引导加载程序中,我使用从 here:

中获取的以下内容为引导加载程序之后的堆栈分配了 4KiB
mov ax, 07c0h           ; 4K stack space after the bootloader -- code is running at 0x07c0
add ax, 288             ; (4096 + 512)/16 bytes per paragraph (288 paragraphs)
mov ss, ax
mov sp, 4096            ; moves the stack pointer

但是现在我需要一种方法来将堆栈分配到我的 OS 代码的末尾,这将是一个未知大小。

我相信我明白这些需要如何设置——类似于使用 es,我使用 ss 来扩展寻址 space,但我不能找到任何能很好地解释我的知识水平的东西。我也不确定如何正确设置它以将我的堆栈放在最后。我用过:

mov ax, stack_start
mov ss, ax
mov sp, 4096

并且没有遇到错误;但是我想知道这是否正确,或者我是否实际上只是在用更高的地址填充内存时为堆栈分配了一些 space 。

ss 是如何运作的?我如何使用它和 sp 在我的代码末尾为堆栈分配内存?

这是在 i386 中使用 nasm。

编辑:如果可能的话,一些验证堆栈是否在正确位置的方法也非常有用。

与其将堆栈放在代码的 end 处,不如将其放在开头。这实际上是引导加载程序的常见做法。换句话说:

 cli
 mov ax,0x7c00
 mov sp, ax
 xor ax, ax
 mov ss, ax
 sti

您现在正在从您的 boatloader 代码加载的地方向下发展。这也使事情变得更容易,因为您接下来要做的事情之一就是加载第二阶段。这使您可以轻松地在同一个程序集文件中创建第二阶段,加载它并跳转到它,而不必担心堆栈覆盖代码。

我知道这对初学者来说很可怕,但是设置堆栈的方法没有好坏之分,只要它满足要求并且没有错误。
在你有一个完全工作的内存管理器之前,你(人)必须是内存管理器。
您必须了解如何使用内存。
"allocating memory" 的整个概念还不存在!你没有内存管理器,你只有一堆 RW RAM 地址。

记录您的假设

从要求您的引导加载程序可以安全承担的最小内存量开始。
由于初始程序加载程序(引导加载程序)位于 0x7c00,因此有理由相信系统具有 31.5KiB 内存。 您还可以假设不存在内存并依赖缓存,但这是高级主题。

当事情出错时,陈述假设是至关重要的。

注意差距

那么你必须知道保留和使用的区域,这是通过标准 memory map 实现的。

摘录:

00000 - 003ff   IVT
00400 - 004ff   BDA
00500 - 0052f   Dangerous Zone (The Petch Zone :) )
00530 - 07bff   Free
07c00 - 07dff   IPL

"Petch Zone" and homage to Michael Petch

做出完全清醒的决定

通过设置临时堆栈来构建您的最小环境。
在上方的片段中,区域 00530 - 07bff 是免费的,您可以将其用作 ~29KiB 堆栈。
由于堆栈是全降序的,您可以将堆栈指针 ss:sp 放在 07c00.
07c00是一个物理地址,将其转换为任何合适的逻辑地址(0000:7c000001:7bf00002:7be00003:7bd0, ..., 07c0:0000,任何人都会选择你最喜欢的那个)并以任何原子方式 w.r.t 设置 SS:SP 给它。打断你 know/like.

EDIT 当偏移量下溢从 0 回绕到 fffe 时,如 正确指出的那样,使用具有小偏移量部分的逻辑地址会导致问题。
虽然静态行为检查(起始地址相同),但动态行为失败,因此最好使用 0000:7c00 并且肯定不要使用 0053 以上的段。

任何其他区域都可以,将堆栈指针设置为 a0000(常规内存的末尾)是另一种选择。
None 更好,只是 知道 你把什么放在哪里。

编辑:正如评论中的,地址a0000也是危险的。
更安全的地址是 9c000

更新内存映射

使用适合您需要的块更新内存映射。
写下内核的开始和结束位置,动态数据所在的位置等等。

例如

00000 - 003ff   IVT
00400 - 004ff   BDA
00500 - 0052f   Dangerous Zone (The Petch Zone :) )
00530 - 07bff   Stack
07c00 - 07dff   IPL
07e00 - 08fff   Kernel
09000 - 10000   Other kernel stuff

到现在为止,您可以在内存映射中使用静态块,但如果您想使用超过 1 MiB 的内存,则需要 query the BIOS 获取可用内存的内存映射。
这本质上是动态的,因为每个系统都带有不同数量的内存。

那个映射只是内存管理器的一个非常小的元数据,所以是时候......

实现一个简单的内存管理器

在您目前设置的基本环境中,编写一个简单的内存管理器,它可以保存内存块。

陷阱:内存管理器需要一些"meta-memory"来保存它的分配内存簿,这需要一个假设 .

一旦可以分配和释放内存,您就可以将堆栈移动到更大的区域,从磁盘或等效设备加载其他数据等等。
这个想法是,您现在可以像在 C 中使用 mallocmfree 那样动态管理内存,从而减轻您在精神上处理内存映射的负担。

更高级的内存管理器

更高级的内存管理器通常是用更容易进行数据操作的高级语言编写的(尤其是在处理诸如分页之类的主题时)。

我在启动代码中做的一件事是将堆栈放在常规内存中尽可能高的位置。

   SYS_MEM    equ 12H
   StkSize    equ 44  ; # of 1024 byte blocks to reserve for stack
     int    SYS_MEM     ; 1K blocks to EBDA
     sub    ax, StkSize ; 1K blocks for stack
     shl    ax, 6       ; * 64 = Base segment of stack

; Align stack on 4k boundary for PLM4 mapping.

     xor    al, al
     mov    es, ax
     xor    cx, cx      ; Guarantees MSB of CX will be NULL
     mov    di, cx      ; ES:DI = Destination
     mov    ch, StkSize*2   ; Block size * 512 = 16 bit words
     or     ax, -1      ; Fill pattern FFFF
     rep    stosw

; DI points to TOS, bump it back for a 96 byte scratch area and 14
; values that were preserved in prolog.

     sub    di, 124 
     mov    si, sp
     mov    cl, 14      ; Registers saved in prolog
    push    di
     rep    ss movsw    ; Copy to new stack frame
     mov    bp, di      ; BP = Base of 96 byte scratch buffer.

; This may look a little odd, but retrieving the value that was saved
; three instructions ago, does work.

     cli
     pop    sp
    push    es
     pop    ss      ; SS:SP = TOS
     sti

这里有一些特别符合我要求的东西,但基本上我在内存顶部获取 44k,这就是堆栈所在的位置,直到我进入 64 位。

然后读取内核的代码移动到540H,内核(1181个扇区)在我的例子中从1000H读入到94600H。我发现这在实模式和保护模式下非常实用。