如何在DOS中获得额外的段?

fuz*_*fuz 4 assembly memory-management dos gnu-assembler x86-16

我想编写一个DOS程序(我的第一个程序),我有点没有经验。

对于该程序,我需要超过64 KB的(传统)内存。如何获得额外的内存?理想情况下,我想为程序增加两个64k的内存块。我可以开始将数据写入地址空间中的某个地方还是需要请求额外的内存吗?

Dav*_*zer 6

在DOS下,可以,您可以开始使用另一段内存。但是,有一个重要的警告!

查看所使用的DOS版本的内存映射。您要确保您没有选择实际上保留用于其他用途的内存区域。这是《多布博士日记》中的一篇:

Address (Hex)                 Memory Usage

0000:0000                Interupt vector table
0040:0000                ROM BIOS data area
0050:0000                DOS parameter area
0070:0000                IBMBIO.COM / IO.SYS *
mmmm:mmmm                BMDOS.COM / MSDOS.SYS *
mmmm:mmmm                CONFIG.SYS - specified information
                         (device drivers and internal buffers
mmmm:mmmm                Resident COMMAND.COM
mmmm:mmmm                Master environment
mmmm:mmmm                Environment block #1
mmmm:mmmm                Application program #1
     .                        .      .                        .      .                        .
mmmm.mmmm                Environment block #n
mmmm:mmmm                Application #n
xxxx:xxxx                Transient COMMAND.COM
A000:0000                Video buffers and ROM
FFFF:000F                Top of 8086 / 88 address space
Run Code Online (Sandbox Code Playgroud)

“官方”内存分配机制是通过内存控制块(MCB)和DOS中断0x21使用0x48进行分配,使用0x49释放内存。在此Microsoft支持文档中可以找到对此的很好的讨论。

有关中断方法的文档,请查看此处。

  • 对于将来的读者:允许您覆盖“ Transient COMMAND.COM”区域。当用户进程覆盖该区域时,该区域将由DOS重新加载。这意味着您可以将CS:0000到A000:0000的所有内存专用于您的进程。 (2认同)

Mic*_*tch 6

我最近偶然发现了这个问题。尽管它已经有几年的历史了,但我觉得当前答案之外的一些额外信息可能对未来的读者有用。


这个问题实际上归结为:我可以随意写入超出 DOS 分配的程序范围的内存吗?这个问题是针对 DOS COM 程序的,但大部分信息也适用于 DOS EXE 程序。

GNU 汇编器的局限性在于它不生成 16 位 DOS EXE 程序,因此您必须生成 DOS COM 程序。原点为 0x100 的 DOS COM 程序。代码、数据和堆栈不能超过 64KiB 的内存(在加载时)。一旦被 DOS 加载器加载到内存中,DOS COM 程序就具有以下特征:

  • 进入时DS=ES=SS=CS
  • 该程序可重定位到任何段,并且不包含加载时间修复/重定位。
  • 即使 DOS COM 程序在加载时被限制为 <= 64KiB 的内存,该程序也会从 DOS 内存池中分配最大的连续空闲块。DOS 加载程序有效地将整个空闲池分配给您的 COM 程序。
  • DOS 加载程序始终设置 SS=CS,但如果我们程序的可用空间量小于 64KiB ,则SP可能会从 0x0000 1以外的值开始。
  • 在将控制权转移到CS:0x0100以启动我们的程序之前,DOS 加载程序总是将 0x0000 的值压入堆栈。CS:0x0000 是 PSP 的开始,PSP 以 2 字节指令 (0xcd 0x20) 开始Int 20hInt 20h终止当前程序。这是允许 DOS COM 程序执行 aret以终止程序的机制。
  • 有一个称为程序段前缀(PSP) 的程序控制块,DOS 将其放置在 CS:0x0000 和 CS:0x0100 之间的内存中
  • COM 程序在 CS:0x0100 开始执行

应该问的第一个问题是:我的 DOS COM 程序实际有多少内存?答案很简单:因人而异。它可能会根据可用的常规内存量而有所不同(IBM PC 通常带有 64KiB、128KiB、256KiB、512KiB 或 640KiB)。另一个答案中引用的 Dr. Dobbs Journal 文章发表于 1988 年,内存映射缺少一些关键的东西。

1987 年,IBM 发布了 IBM PS/2 系列计算机。为了保存鼠标相关信息,IBM 意识到中断向量表上方的BIOS 数据区空间不足,因此创建了扩展 BIOS 数据区(EBDA)。此内存由 BIOS 保留,IBM PS/2 BIOS 开始报告内存减少 1KiB(639KiB 而不是 640KiB)。EBDA 的大小可能因 BIOS 制造商而异。BIOSInt 12h调用将返回不包括 EBDA 区域的常规内存量 (<=640KiB)。DOS 依赖于此来确定它可以使用多少内存。

更糟糕的是,当基于 386SL 的系统发布时,它包括在 ring -2 运行的系统管理模式,并且可以完全访问您的 PC。这些系统也开始使用 EBDA 中的空间。一些系统需要超过 1KiB。理论上,您可以拥有 128KiB 的 EBDA 空间,尽管我不确定是否有任何系统拥有过!该区域最终用于电源管理 (APM)、ACPI、SMBIOS,并且系统管理模式可以随时写入该区域。出于这个原因,该区域通常被操作系统视为保留。实际发生的情况取决于 BIOS 和机器的制造商。

除了 EBDA,一些 DOS 程序(和恶意软件)会拦截 BIOS Int 12h 并报告较少的内存,以隐藏(或驻留)一段 DOS 不应触及的代码/数据。Dr. Dobbs 内存映射可以使用几个附加项:

mmmm:mmmm                Environment block #1
mmmm:mmmm                Application program #1
     .                        .      .                        .      . 
mmmm.mmmm                Environment block #n
mmmm:mmmm                Application #n
xxxx:xxxx                Transient COMMAND.COM
hhhh:hhhh                Hidden/Resident programs and data
eeee:eeee                Extended BIOS Data Area
A000:0000                Video buffers and ROM
FFFF:000F                Top of 8086 / 88 address space
Run Code Online (Sandbox Code Playgroud)

这个故事的寓意:您不应该假设可用的内存量在CS:0x00000xa000:0x00002之间运行。

要回答有关如何判断程序专用的内存区域的问题,可以通过查看 PSP 来回答,尤其是 offset 处的 WORD 值CS:0x0002

02h-03h 字(2 个字节) 超出分配给程序的内存的第一个字节段

通过读取该值,您可以获得第一个字节的段,该段刚好超出您的程序已分配的范围(我们称之为NEXTSEG)。通常NEXTSEG为 0xA000 或 0x9FC0(具有 1KiB EBDA 的系统将具有此值)。由于前面讨论的原因,它会因硬件而异。该区域将与 MS-DOS 的 COMMAND.COM 的瞬态部分重叠。实际上,我们可以保证在加载后专供我们的 COM 程序使用的内存区域是我们可以自由使用CS:0x0000和之间的所有物理内存NEXTSEG:0x0000


COM 程序分配 128KiB

由于20 位段的重叠性质:偏移寻址每个段指向内存中称为段落的不同 16 字节区域的开始。将一个段增加 1 会在内存中增加 16 个字节,而减少则返回 16 个字节。这对于执行所需的算术以找出我们的程序需要多少并确保有足够的内存来满足请求非常重要。

128KiB 是 128*1024/16=8192 段。我们的 COM 程序加载到的区域(以及放置堆栈的位置)的实际大小受 CS:0x0000 和堆栈 ( SP ) 指向的位置之外的段的限制。由于 DOS 总是ret为 COM 程序推送一个 2 字节的值(将返回的返回地址) - 下一段可以通过将SP除以 16(或 SHR 除以 4)并加 1(我们称之为SEGAFTERSTACK)来计算。

最简单的方法是将我们的 128KiB 数据放置在堆栈的上边缘之外 ( SEGAFTERSTACK)。我们只需要确保SEGAFTERSTACKNEXTSEG(DOS 给我们的程序区域的范围)之间有足够的空间。如果该值 >=8192 个段落,那么我们有足够的内存,我们可以自由访问它,因为我们认为合适。如果我们确实有足够的内存,我们可以要求 DOS 将我们的 COM 程序大小调整到我们需要使用的确切空间量Int 21h/AH=4ah。我们不需要调整 DOS 已经为我们分配的内存大小,但是如果您的代码需要使用 DOS 的 Exec 函数加载/运行子程序,它会很有用Int 21h/AH=4bh

注意: DOS < 2.0 不支持内存控制块,这意味着Int 21h分配、释放和调整大小的功能不可用。在 DOS < 2.0 上调用它们将无声无息地失败。当调整大小减少程序在内存中的大小时,该函数不应失败,因此我们应该能够忽略任何错误。

使用 GNU 汇编器的程序版本可确保在堆栈之后我们的程序有 128KiB 的可用空间,可能如下所示:

EXTRA_SIZE      = 128*1024     # Allocate 128KiB above stack
PARA_SIZE       = 16           # A paragraph = 16 bytes
EXTRA_SIZE_PARA = (EXTRA_SIZE+PARA_SIZE-1)/PARA_SIZE
                               # Extra Size in Paragraphs
COM_ORG         = 0x100        # Origin point for COM  program is 0x100

.code16
.global _start
.section .text

_start:
    # In a COM program CS=DS=ES=SS=0x0000. IP=0x100. The PSP is a 0x100 byte structure
    # between CS:0x0000 and CS:0x0100. DOS allocates the largest free block of
    # contiguous conventional memory from the DOS memory pool to our COM program. 
    # SS:SP grows down from the last paragraph allocated to us OR the top of the
    # 64kb segment, whichever is lower.
    #
    # At (DS:[0x0002]) is the segment (NEXTSEG) of the first byte  beyond the memory
    # allocated to our program. This means our program has been allocated all memory
    # between CS:0x0000 and NEXTSEG:0x0000

    # Get the next segment just above the top of the stack
    mov %sp, %bp               # BP = Current stack pointer
    mov $4, %cl                # Compute the segment just above top of stack
                               # Where extra data will be placed
    shr %cl, %bp               #     Divide BP by 16
    inc %bp                    #     and add 1

    # Compute a new program size including extra data area we want and
    # place it above the stack
    lea EXTRA_SIZE_PARA(%bp), %bx
                               # BX = Size (paragraphs) of Code/Data+Stack+Extra Data
    mov 0x0002, %ax            # Get the segment above last allocated
                               #     paragraph of our program from PSP @ [DS:0002]
    sub %bx, %ax               # Do we have enough memory for the extra data?
    jb .no_mem                 #     If not  display memory error and exit
    mov $0x4a, %ah             # Request DOS resize our program's memory block
    int $0x21                  #     to exactly the # of paragraphs we need.
    push %cs
    pop %bx                    # BX = CS (first segment of our program)
    add %bx, %bp               # BP = segment at the start of our extra data

    # Do stuff. Just an example:
    lea 0x0000(%bp), %si       # SI=segment of first 64KiB segment we allocated
    lea 0x1000(%bp), %di       # DI=segment of second 64KiB segment we allocated

    jmp .exit

.no_mem:
    mov $no_mem_str, %dx       # Have DOS print an error and exit.
    mov $9, %ah
    int $0x21

.exit:
    ret                        # We're done

no_mem_str: .asciz "Out of memory\n\r$"

_end:
Run Code Online (Sandbox Code Playgroud)

一个稍微复杂一点的变体是将我们默认给定的堆栈大小调整为适合我们工作的大小,然后将 128KiB 的额外数据放在堆栈之后。我们需要计算我们的代码和数据的范围,以将堆栈放置在它之上,然后是用于 128KiB 数据的内存。这段代码使用 4096 字节的堆栈来做到这一点:

STACK_SIZE = 4096              # Stack size = 4KiB
EXTRA_SIZE = 128*1024          # Allocate 128KiB above stack
PARA_SIZE  = 16                # A paragraph = 16 bytes
COM_ORG    = 0x100             # Origin point for COM  program is 0x100

.code16
.global _start
.section .text

_start:
    # In a COM program CS=DS=ES=SS=0x0000. IP=0x100. The PSP is a 0x100 byte structure
    # between CS:0x0000 and CS:0x0100. DOS allocates the largest free block of
    # contiguous conventional memory from the DOS memory pool to our COM program. 
    # SS:SP grows down from the last paragraph allocated to us OR the top of the
    # 64kb segment, whichever is lower.

    # At (DS:[0x0002]) is the segment (NEXTSEG) of the first byte  beyond the memory
    # allocated to our program. This means our program has been allocated all memory
    # between CS:0x0000 and NEXTSEG:0x0000
    
    push %ds
    pop %cx                    # CX = Segment at start of our program
    mov %cx, %bp               # BP = A copy (for later) of program starting segment
    mov $PROG_SIZE_PARA, %bx   # BX = number of paragraphs of EXTRA memory to allocate 
    add %bx, %cx               # CX = total number of paragraphs our program needs
    mov 0x0002, %ax            # AX = next segment past end of our program
                               #     retrieved from our program's PSP @ [DS:0002]
    sub %cx, %ax               # Do we have enough memory to satisfy the request?
    jb .no_mem                 #     If not  display memory error and exit
    mov $0x4a, %ah             # Request DOS resize our programs memory block
    int $0x21                  #     to exactly the # of paragraphs we need.

    mov $STACK_TOP_OFS, %sp    # Place the stack after non-BSS code and data
                               #     and before the BSS (Extra) memory
    xor %ax, %ax               # Push a 0x0000 return address as DOS does for us
    push %ax                   #     when initializing our program. Memory address
                               #     CS:0x0000 contains an Int 20h instruction to exit
    add $EXTRA_SEG, %bp        # BP = segment where our extra data areas starts

    # Do stuff. Just an example:    
    lea 0x0000(%bp), %si       # SI=segment of first 64KiB segment we allocated
    lea 0x1000(%bp), %di       # DI=segment of second 64KiB segment we allocated

    jmp .exit

.no_mem:
    mov $no_mem_str, %dx       # Have DOS print an error and exit.
    mov $9, %ah
    int $0x21

.exit:
    ret                        # We're done

no_mem_str: .asciz "Out of memory\n\r$"

_end:

# Length of non-BSS Code and Data
CODE_DATA_LEN   = _end-_start

# Segment number after the PSP/code/non-BSS data/stack relative to start of program
EXTRA_SEG       = (CODE_DATA_LEN+COM_ORG+STACK_SIZE+PARA_SIZE-1)/PARA_SIZE

# Size of the total program in paragraphs
PROG_SIZE_PARA  = EXTRA_SEG+EXTRA_SIZE_PARA

# New Stack offset(SP) will be moved just below extra data
STACK_TOP_OFS   = EXTRA_SEG*PARA_SIZE

# Size of the extra memory region in paragraphs
EXTRA_SIZE_PARA = (EXTRA_SIZE+PARA_SIZE-1)/PARA_SIZE
Run Code Online (Sandbox Code Playgroud)

这些样本可以组装并链接到一个程序,调用myprog.com

as --32 myprog.s -o myprog.o
ld -melf_i386 -Ttext=0x100 --oformat=binary myprog.o -o myprog.com
Run Code Online (Sandbox Code Playgroud)

在 DOS EXE 程序中分配 128KiB

DOS 加载器还加载 EXE 程序(它们有一个MZ 头文件)。MZ 标头包含程序信息、重定位表、堆栈、入口点以及可执行文件中物理存在的数据之外的最小和最大内存分配要求。具有完全未初始化数据的段(包括但不限于 BSS 和 Stack 段)不占用可执行文件中的空间,但 DOS 加载器被告知通过MINALLOCMAXALLOC头字段分配额外的内存:

MINALLOC。这个词表示程序开始执行所需的最少段落数。这是对容纳加载模块所需的内存的补充。该值通常表示在程序末尾链接的任何未初始化数据和/或堆栈段的总大小。这个空间不直接包含在加载模块中,因为没有特定的初始化值,它只会浪费磁盘空间。

MAXALLOC。该字表示程序在开始执行之前希望分配给它的最大段落数。这表示超出加载模块所需的额外内存和 MINALLOC 指定的值。如果不能满足请求,程序将分配尽可能多的可用内存

MINALLOC 是 EXE 本身中所需的代码和数据上方的段落数。MAXALLOC 总是至少等于 MINALLOC 但如果 (MAXALLOC > MINALLOC) 那么 DOS 将尝试满足对附加段落的请求 (MAXALLOC-MINALLOC)。如果不能满足该请求,则 DOS 将分配它确实拥有的所有可用空间。许多工具和编程语言通常将 MAXALLOC 和 MINALLOC 之间的额外内存称为HEAP

值得注意的是,生成设置 MINALLOC 和 MAXALLOC 的可执行文件是最终的链接过程。通常,链接器默认将 MAXALLOC 设置为 0xffff,有效地请求 HEAP 占用与 DOS 可以分配的尽可能多的连续空间。该EXEMOD程序旨在允许对此进行更改:

EXEMOD

EXEMOD 显示或更改 DOS 文件头中的字段。要使用此实用程序,您必须了解文件头的 DOS 约定

[剪辑]

/MIN n 将最小分配值设置为 n,其中 n 是设置段落数的十六进制值。如果需要调整以适应堆栈,则实际设置的值可能与请求的值不同。

/MAX n

将最大分配设置为 n,其中 n 是设置段落数的十六进制值。最大分配值必须大于或等于最小分配值。此选项与链接器参数 ICPARMAXALLOC 具有相同的效果。

在没有内存控制块概念的 DOS < 2.0 中,使用EXEMOD是更改 DOS 可执行文件的额外内存要求的方法。在 DOS 2.0+ 中,程序(在运行时)可以通过 DOSInt 21h函数分配新内存、调整内存大小和释放内存。

对于此讨论,程序需要128KiB 的额外内存,因此示例将将该数据放置在未初始化的数据中。链接/可执行文件生成过程将通过添加所需的额外段落来调整 MZ 标头中的 MINALLOC 字段。

希望分配 128KiB(两个 64KiB 段一个接一个放置)的 DOS 程序的第一个示例是用FASM汇编编写的:

format MZ                      ; DOS EXE Program

stack 4096                     ; 4KiB stack. FASM puts stack after BSS data

entry code:main                ; Program entry point (seg:offset)

segment code
main:
    push ds
    pop ax
    mov bx, EndSeg
    sub bx, ax                 ; BX = size of program in paragraphs (EndSeg-DS)
    mov ah, 4ah                ; Resize to the number of paragraphs we need
    int 21h                    ;     because the DOS loader sometimes allocates slightly
                               ;     more than our actual program requirements

    ; Do Stuff. Just an example:    
    mov si, ExtraSeg1          ; SI=segment of first 64KiB segment we allocated
    mov di, ExtraSeg2          ; DI=segment of second 64KiB segment we allocated

    mov ax, 4c00h              ; We're done, have DOS exit and return 0
    int 21h

segment ExtraSeg1
rb 65536                       ; Reserve 65536 uninitialized "bytes" in BSS area

segment ExtraSeg2
rb 65536                       ; Reserve 65536 uninitialized "bytes" in BSS area

segment EndSeg                 ; Use this segment to determine last segment of our program
                               ;     Segments with no data will be put in BSS after
                               ;     other BSS segments
Run Code Online (Sandbox Code Playgroud)

适用于大多数 MASM/JWASM/TASM 版本的版本如下所示:

.model compact, C              ; Multiple data segments, one code segment
.stack 4096                    ; 4KiB stack

; fardata? are uninitialized segments (like BSS)
.fardata? ExtraSeg1            ; Allocate first 64KiB in a new far segment
db 65535 DUP(?)                ; Some old assemblers don't support 65536! Set to 65535
                               ; The next segment will be aligned to a paragraph boundary
                               ; Uninitialized data `?` will not be physically in our EXE

.fardata? ExtraSeg2            ; Allocate second 64KiB in a new far segment after first
db 65535 DUP(?)                ; Some old MASM assemblers don't support 65536! Set to 65535
                               ; The next segment will be aligned to a paragraph boundary
                               ; Uninitialized data `?` will not be physically in our EXE


.fardata? EndSeg               ; Use this segment to determine last segment of our program
                               ;     Segments with no data will be put in BSS after
                               ;     other BSS segments
.code
main PROC
    push ds
    pop ax
    mov bx, EndSeg
    sub bx, ax                 ; BX = size of program in paragraphs (EndSeg-DS)
    mov ah, 4ah                ; Resize to the number of paragraphs we need
    int 21h                    ;     because the DOS loader sometimes will allocate 
                               ;     slightly more than our actual program requirements

    ; Do Stuff. Just an example:
    mov si, ExtraSeg1          ; SI=segment of first 64KiB segment we allocated
    mov di, ExtraSeg2          ; DI=segment of second 64KiB segment we allocated

    mov ax, 4c00h              ; We're done, have DOS exit and return 0
    int 21h
main ENDP

END main                       ; Program entry point is main
Run Code Online (Sandbox Code Playgroud)

脚注:

  • 1当可供 DOS 使用的空闲内存少于 64KiB 时,SP将被设置为从低于 DOS 可用空闲内存顶部的偏移量增长。当有 64KiB 或更多可用内存时,DOS 加载程序将SP设置为 0x0000。在 >= 64KiB 可用内存的情况下,第一次推送数据(返回地址 0x0000)将SP包装到段的顶部 0xfffe (0x0000-2)。这是一个实模式怪癖:如果您将SS:SP设置为 SS:0x0000,则推送的第一个值将放置在SS段顶部的 SS:0xFFFE 处。
  • 2虽然0xa000:0x0000通常被视为 DOS 可用的连续常规内存的高端,但它不一定是那样。一些内存管理器(JEMMEX、QEMM、386Max 等等)和他们的工具可以成功移动 EBDA(在不会引起问题的设备上)并且可以被告知 VGA/EGA 内存在 0xa000:0x0000 到 0xa000 :0xffff 未使用可以将 DOS 分配的连续内存的上端移动到 0xb000:0x0000。在无头(无视频)配置中甚至可以拥有更多。执行此操作的 386 内存管理器通常以 v8086 模式运行 DOS,并将扩展内存(使用 386 对分页的支持)重新映射到 0xa000:0x0000 和 0xf000:0xffff 之间的未使用区域。


Dir*_*omp 5

如果我们启动一个程序,DOS 会将所有空闲内存都给该程序,因此我们必须在请求新内存之前将其还给 DOS。第一步是计算我们的程序所需的内存,并将其余的还给 DOS。这部分我们必须放在程序的开头,在操作 SS、SP 和 ES 之前。

mov      bx, ss
mov      ax, es
sub      bx, ax
mov      ax, sp
add      ax, 0Fh
shr      ax, 4
add      bx, ax
mov      ah, 4Ah
int    21h
Run Code Online (Sandbox Code Playgroud)

下一步是请求新的内存。

mov      bx, 2000h ; 128 KB
mov      ah, 48h
int    21h
jc  NOSPACE
; AX = segment address
Run Code Online (Sandbox Code Playgroud)

  • 我们必须用 4Ah int 21h 将空闲内存还给 DOS,然后才能用 48h int 21h 请求新内存,因为我们并不确切知道哪些段是空闲的,哪些不是。程序后的以下128 KB是免费的,这是不安全的。如果调用后未设置进位标志,则只有使用 48h int 21h 我们才能保证所请求的内存量可以自由使用。 (2认同)