如何在 DOS 中获得额外的段?
How can I get an extra segment in DOS?
我想写一个小的 DOS 程序(我的第一个),我有点缺乏经验。
对于程序,我需要超过 64 KB 的(常规)内存。我怎样才能获得额外的内存?理想情况下,我希望为程序提供两个额外的 64k 内存块。我可以直接开始将数据写入地址 space 的某处,还是需要申请额外的内存?
在DOS下,是的,你可以开始使用另一段内存。然而,有一个重要的警告!
查看您正在使用的 DOS 版本的内存映射。您要确保没有选择实际上为其他目的保留的内存区域。这是 Dr. Dobb's 期刊中的一篇:
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
"official" 内存分配机制是通过内存控制块 (MCB) 和 DOS 中断 0x21 使用 0x48 分配和 0x49 释放内存。可以在 Microsoft support document.
中找到对此的很好的讨论
有关中断方法的文档,you might look here.
如果我们启动一个程序,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
下一步是申请新内存。
mov bx, 2000h ; 128 KB
mov ah, 48h
int 21h
jc NOSPACE
; AX = segment address
您可以通过将其中一个段寄存器设置为所需的值来获得您想要的任何段。但请记住
- 每个段都从 16 字节边界开始,这意味着 0400(偏移量:0000)的段将等于 0040(偏移量:3c00)的段和 0000(偏移量:4000)的另一个段,并且等等
- 这些范围重叠,也就是说段寄存器每增加1,绝对内存地址就会增加16。
- BIOS 或其他外围设备预设和使用的范围在
的另一个答案中进行了详细说明
- 确保您的片段大小为 64kb,并且不与其他片段重叠
我最近偶然发现了这个问题。尽管它已经有几年历史了,但我觉得除了当前答案之外的一些额外信息可能对未来的读者有用。
这个问题真的可以归结为:我可以任意写入超出 DOS 分配给我的程序范围的内存吗?这个问题是针对 DOS COM 程序的,但是大部分信息也适用于 DOS EXE 程序。
GNU 汇编程序的局限性在于它不能生成 16 位 DOS EXE 程序,因此您必须生成 DOS COM 程序。原点为 0x100 的 DOS COM 程序。代码、数据和堆栈不能超过 64KiB 内存(加载时)。 DOS COM 程序一旦被 DOS 加载程序加载到内存中就具有这些特征:
- 进入后DS=ES=SS=CS.
- 程序可重定位到任何段,不包含加载时间fixups/relocations。
- 程序从 DOS 内存池中分配了最大的连续空闲块,即使 DOS COM 程序在加载时限制为 <= 64KiB 内存。 DOS 加载程序有效地将整个空闲池分配给您的 COM 程序。
- DOS 加载程序总是设置 SS=CS,但是 SP 可能会从 0x00001 以外的值开始,如果可用的数量space 因为我们的程序小于 64KiB。
- DOS 加载程序总是在将控制权转移到 CS:0x0100 以启动我们的程序之前将值 0x0000 压入堆栈。 CS:0x0000 是 PSP 的开始,PSP 以 2 字节指令 (0xcd 0x20)
Int 20h
开始。 Int 20h
终止当前程序。这是允许 DOS COM 程序执行 ret
来终止程序的机制。
- DOS在内存中CS:0x0000和CS:0x0100
之间有一个叫做Program Segment Prefix(PSP)的程序控制块
- COM 程序在 CS:0x0100
开始执行
第一个应该问的问题是:我的 DOS COM 程序实际有多少内存?简单的答案是:它会有所不同。它可能会根据可用的常规内存量而有所不同(IBM PC 通常配备 64KiB、128KiB、256KiB、512KiB 或 640KiB)。另一个答案中引用的 Dr. Dobbs Journal 文章发表于 1988 年,内存映射缺少一些关键的东西。
1987 年,IBM 发布了 IBM PS/2 系列计算机。为了保存鼠标相关信息,IBM 意识到 BIOS Data Area above the interrupt vector table so they created an Extended BIOS Data Area (EBDA). This memory is reserved by the BIOS, and the IBM PS/2 BIOS started reporting 1KiB less memory (639KiB instead of 640KiB). The EBDA can be of varying sizes depending on the BIOS manufacturer. The BIOS Int 12h
调用中 space 不足 return 常规内存量 (<=640KiB),不包括 EBDA 区域。 DOS 依靠它来确定它可以使用多少内存。
更糟糕的是,当基于 386SL 的系统发布时,它包含 System Management Mode,它在 ring -2 上运行并且可以完全访问您的 PC。这些系统也开始在 EBDA 中使用 space。一些系统需要超过 1KiB。从理论上讲,您可以拥有 128KiB 的 EBDA space,尽管我不确定是否有任何系统曾经拥有它!该区域最终用于电源管理 (APM)、ACPI、SMBIOS,系统管理模式可以随时写入该区域。由于这个原因,这个区域通常被操作系统认为是保留的。实际发生的情况取决于 BIOS 和机器的制造商。
在 EBDA 之外,一些 DOS 程序(和恶意软件)会拦截 BIOS Int 12h 并报告较少的内存,以隐藏(或驻留)DOS 不应接触的 code/data 部分。 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
故事的寓意:您不应该假设可用内存量在 CS:0x0000
和 0xa000:0x0000
之间运行2.
要回答关于如何判断哪个内存区域是您的程序独有的问题,可以通过查看 PSP 来回答,尤其是偏移量 CS:0x0002
:[=58= 处的 WORD 值]
02h-03h word (2 bytes) Segment of the first byte beyond the memory allocated to the program
通过读取这个值,您可以获得第一个字节的段,刚好超出您的程序已分配的段(我们称之为 NEXTSEG
)。通常 NEXTSEG
将是 0xA000 或 0x9FC0(具有 1KiB EBDA 的系统将具有此值)。由于前面讨论的原因,它会因硬件而异。该区域将与 MS-DOS 的 COMMAND.COM 的瞬态部分重叠。实际上,我们可以保证COM程序加载后独占的内存区域是我们可以自由使用CS:0x0000
和NEXTSEG:0x0000
之间的所有物理内存。
COM程序分配128KiB
由于 20-bit segment:offset addressing 的重叠性质,每个段都指向内存中称为 段落 的不同 16 字节区域的开始。将一个段递增 1 在内存中前进 16 个字节,递减返回 16 个字节。这对于执行所需的算法以找出我们的程序需要多少并确保有足够的内存来满足请求很重要。
128KiB 为 128*1024/16=8192 帕图表。我们的 COM 程序加载到的区域的实际大小(以及放置堆栈的位置)以 CS:0x0000 为界,而堆栈 (SP) 之外的段是指向。由于 DOS 总是为 COM 程序推送一个 2 字节值(ret
将 return 到的 return 地址)- 下一段可以通过除以 SP 乘以 16(或 SHR 乘以 4)并加 1(我们称之为 SEGAFTERSTACK
)。
最简单的做法是将我们的 128KiB 数据放在堆栈的上边缘之外 (SEGAFTERSTACK
)。我们只需要确保在 SEGAFTERSTACK
和 NEXTSEG
之间有足够的 space (DOS 给我们的程序区域的范围)。如果该值 >=8192 个段落,那么我们有足够的内存,我们可以随意访问它。如果我们确实有足够的内存,我们可以使用 Int 21h/AH=4ah
. We don't need to resize the memory DOS already allocated for us but it can be useful if your code needs to load/run a child program with DOS's Exec function Int 21h/AH=4bh
.
要求 DOS 将我们的 COM 程序的大小调整到我们需要的 space 的确切数量
注意: DOS < 2.0 不支持 Memory Control Blocks,这意味着分配、释放和调整大小的 Int 21h
函数不可用。在 DOS < 2.0 上调用它们将无提示地失败。当调整大小时减少程序在内存中的大小时,该函数不应失败,因此我们应该能够忽略任何错误。
一个使用 GNU 汇编程序的程序版本,确保我们在堆栈之后有 128KiB 的空闲空间 space 可能如下所示:
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 , %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 [=11=]x4a, %ah # Request DOS resize our program's memory block
int [=11=]x21 # 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 , %ah
int [=11=]x21
.exit:
ret # We're done
no_mem_str: .asciz "Out of memory\n\r$"
_end:
一个稍微复杂一点的变体是将我们默认给定的堆栈大小调整为适合我们工作的大小,然后将 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 [=12=]x4a, %ah # Request DOS resize our programs memory block
int [=12=]x21 # 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 , %ah
int [=12=]x21
.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
这些示例可以组装并链接到名为 myprog.com
的程序,其中:
as --32 myprog.s -o myprog.o
ld -melf_i386 -Ttext=0x100 --oformat=binary myprog.o -o myprog.com
在 DOS EXE 程序中分配 128KiB
DOS 加载程序也加载 EXE 程序(它们有一个 MZ header)。 MZ header 包含程序信息、重定位表、堆栈、入口点以及超出可执行文件中实际存在的数据的最小和最大内存分配要求。完全未初始化数据的段(包括但不限于BSS和Stack段)在可执行文件中不占用space,但DOS加载器被告知通过MINALLOC[=分配额外的内存154=] 和 MAXALLOC header 字段:
MINALLOC.
This word indicates the minimum number of paragraphs the program requires to begin execution. This is in addition to the memory
required to hold the load module. This value normally represents the
total size of any uninitialised data and/or stack segments that are
linked at the end of a program. This space is not directly included in
the load module, since there are no particular initialising values and
it would simply waste disk space.
MAXALLOC. This word indicates
the maximum number of paragraphs that the program would like allocated
to it before it begins execution. This indicates additional memory
over and above that required by the load module and the value
specified by MINALLOC. If the request cannot be satisfied, the program
is allocated as much memory as is available
MINALLOC 是 EXE 本身 需要 的代码和数据之上的段落数。 MAXALLOC 始终至少等于 MINALLOC,但如果 (MAXALLOC > MINALLOC),则 DOS 将尝试满足附加段落的请求 (MAXALLOC-MINALLOC)。如果不能满足该请求,那么 DOS 将分配它确实拥有的所有可用 space。通常,MAXALLOC 和 MINALLOC 之间的额外内存被许多工具和编程语言称为 HEAP。
值得注意的是,它是生成设置MINALLOC 和MAXALLOC 的可执行文件的最终链接过程。默认情况下,链接器通常将 MAXALLOC 设置为 0xffff,有效地请求 HEAP 占用 DOS 可以分配的尽可能多的连续 space。 EXEMOD
程序旨在允许更改此内容:
EXEMOD
EXEMOD 显示或更改 DOS 文件中的字段 header。使用
此实用程序,您必须了解文件 header
的 DOS 约定
[snip]
/MIN n
Sets the minimum allocation value to n, where n is a
hexadecimal value setting the number of paragraphs. The
actual value set may be different from the requested value
if adjustments are necessary to accommodate the stack.
/MAX n
Sets the maximum allocation to n, where n is a
hexadecimal value setting the number of paragraphs. The
maximum allocations value must be greater than or equal
to the minimum allocation value. This option has the
same effect as the linker parameter ICPARMAXALLOC.
在没有内存控制块概念的 DOS < 2.0 中,使用 EXEMOD
是更改 DOS 可执行文件的额外内存要求的方法。在 DOS 2.0+ 中,程序(位于 run-time)可以通过 DOS Int 21h
函数分配新内存、调整内存大小和释放内存。
对于此讨论,程序 需要 128KiB 的额外内存,因此示例将把该数据放在未初始化的数据中。 linking/executable 生成过程将通过添加所需的额外段落来调整 MZ header 中的 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
适用于大多数 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
脚注:
- 1当 DOS 剩余可用内存少于 64KiB 时,SP 将被设置为从偏移量向下增长在 DOS 可用空闲内存的顶部下方。当有 64KiB 或更多可用内存时,DOS 加载程序将 SP 设置为 0x0000。在 >= 64KiB 可用内存的情况下,第一次推送数据(return 地址 0x0000)将 SP 包装到 0xfffe(0x0000-2)段的顶部).这是一个实模式的怪癖:如果你设置 SS:SP 到 SS:0x0000 推送的第一个值将被放置在SS:0xFFFE 在 SS 段的顶部。
- 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 之间的未使用区域。
我想写一个小的 DOS 程序(我的第一个),我有点缺乏经验。
对于程序,我需要超过 64 KB 的(常规)内存。我怎样才能获得额外的内存?理想情况下,我希望为程序提供两个额外的 64k 内存块。我可以直接开始将数据写入地址 space 的某处,还是需要申请额外的内存?
在DOS下,是的,你可以开始使用另一段内存。然而,有一个重要的警告!
查看您正在使用的 DOS 版本的内存映射。您要确保没有选择实际上为其他目的保留的内存区域。这是 Dr. Dobb's 期刊中的一篇:
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
"official" 内存分配机制是通过内存控制块 (MCB) 和 DOS 中断 0x21 使用 0x48 分配和 0x49 释放内存。可以在 Microsoft support document.
中找到对此的很好的讨论有关中断方法的文档,you might look here.
如果我们启动一个程序,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
下一步是申请新内存。
mov bx, 2000h ; 128 KB
mov ah, 48h
int 21h
jc NOSPACE
; AX = segment address
您可以通过将其中一个段寄存器设置为所需的值来获得您想要的任何段。但请记住
- 每个段都从 16 字节边界开始,这意味着 0400(偏移量:0000)的段将等于 0040(偏移量:3c00)的段和 0000(偏移量:4000)的另一个段,并且等等
- 这些范围重叠,也就是说段寄存器每增加1,绝对内存地址就会增加16。
- BIOS 或其他外围设备预设和使用的范围在
- 确保您的片段大小为 64kb,并且不与其他片段重叠
我最近偶然发现了这个问题。尽管它已经有几年历史了,但我觉得除了当前答案之外的一些额外信息可能对未来的读者有用。
这个问题真的可以归结为:我可以任意写入超出 DOS 分配给我的程序范围的内存吗?这个问题是针对 DOS COM 程序的,但是大部分信息也适用于 DOS EXE 程序。
GNU 汇编程序的局限性在于它不能生成 16 位 DOS EXE 程序,因此您必须生成 DOS COM 程序。原点为 0x100 的 DOS COM 程序。代码、数据和堆栈不能超过 64KiB 内存(加载时)。 DOS COM 程序一旦被 DOS 加载程序加载到内存中就具有这些特征:
- 进入后DS=ES=SS=CS.
- 程序可重定位到任何段,不包含加载时间fixups/relocations。
- 程序从 DOS 内存池中分配了最大的连续空闲块,即使 DOS COM 程序在加载时限制为 <= 64KiB 内存。 DOS 加载程序有效地将整个空闲池分配给您的 COM 程序。
- DOS 加载程序总是设置 SS=CS,但是 SP 可能会从 0x00001 以外的值开始,如果可用的数量space 因为我们的程序小于 64KiB。
- DOS 加载程序总是在将控制权转移到 CS:0x0100 以启动我们的程序之前将值 0x0000 压入堆栈。 CS:0x0000 是 PSP 的开始,PSP 以 2 字节指令 (0xcd 0x20)
Int 20h
开始。Int 20h
终止当前程序。这是允许 DOS COM 程序执行ret
来终止程序的机制。 - DOS在内存中CS:0x0000和CS:0x0100 之间有一个叫做Program Segment Prefix(PSP)的程序控制块
- COM 程序在 CS:0x0100 开始执行
第一个应该问的问题是:我的 DOS COM 程序实际有多少内存?简单的答案是:它会有所不同。它可能会根据可用的常规内存量而有所不同(IBM PC 通常配备 64KiB、128KiB、256KiB、512KiB 或 640KiB)。另一个答案中引用的 Dr. Dobbs Journal 文章发表于 1988 年,内存映射缺少一些关键的东西。
1987 年,IBM 发布了 IBM PS/2 系列计算机。为了保存鼠标相关信息,IBM 意识到 BIOS Data Area above the interrupt vector table so they created an Extended BIOS Data Area (EBDA). This memory is reserved by the BIOS, and the IBM PS/2 BIOS started reporting 1KiB less memory (639KiB instead of 640KiB). The EBDA can be of varying sizes depending on the BIOS manufacturer. The BIOS Int 12h
调用中 space 不足 return 常规内存量 (<=640KiB),不包括 EBDA 区域。 DOS 依靠它来确定它可以使用多少内存。
更糟糕的是,当基于 386SL 的系统发布时,它包含 System Management Mode,它在 ring -2 上运行并且可以完全访问您的 PC。这些系统也开始在 EBDA 中使用 space。一些系统需要超过 1KiB。从理论上讲,您可以拥有 128KiB 的 EBDA space,尽管我不确定是否有任何系统曾经拥有它!该区域最终用于电源管理 (APM)、ACPI、SMBIOS,系统管理模式可以随时写入该区域。由于这个原因,这个区域通常被操作系统认为是保留的。实际发生的情况取决于 BIOS 和机器的制造商。
在 EBDA 之外,一些 DOS 程序(和恶意软件)会拦截 BIOS Int 12h 并报告较少的内存,以隐藏(或驻留)DOS 不应接触的 code/data 部分。 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
故事的寓意:您不应该假设可用内存量在 CS:0x0000
和 0xa000:0x0000
之间运行2.
要回答关于如何判断哪个内存区域是您的程序独有的问题,可以通过查看 PSP 来回答,尤其是偏移量 CS:0x0002
:[=58= 处的 WORD 值]
02h-03h word (2 bytes) Segment of the first byte beyond the memory allocated to the program
通过读取这个值,您可以获得第一个字节的段,刚好超出您的程序已分配的段(我们称之为 NEXTSEG
)。通常 NEXTSEG
将是 0xA000 或 0x9FC0(具有 1KiB EBDA 的系统将具有此值)。由于前面讨论的原因,它会因硬件而异。该区域将与 MS-DOS 的 COMMAND.COM 的瞬态部分重叠。实际上,我们可以保证COM程序加载后独占的内存区域是我们可以自由使用CS:0x0000
和NEXTSEG:0x0000
之间的所有物理内存。
COM程序分配128KiB
由于 20-bit segment:offset addressing 的重叠性质,每个段都指向内存中称为 段落 的不同 16 字节区域的开始。将一个段递增 1 在内存中前进 16 个字节,递减返回 16 个字节。这对于执行所需的算法以找出我们的程序需要多少并确保有足够的内存来满足请求很重要。
128KiB 为 128*1024/16=8192 帕图表。我们的 COM 程序加载到的区域的实际大小(以及放置堆栈的位置)以 CS:0x0000 为界,而堆栈 (SP) 之外的段是指向。由于 DOS 总是为 COM 程序推送一个 2 字节值(ret
将 return 到的 return 地址)- 下一段可以通过除以 SP 乘以 16(或 SHR 乘以 4)并加 1(我们称之为 SEGAFTERSTACK
)。
最简单的做法是将我们的 128KiB 数据放在堆栈的上边缘之外 (SEGAFTERSTACK
)。我们只需要确保在 SEGAFTERSTACK
和 NEXTSEG
之间有足够的 space (DOS 给我们的程序区域的范围)。如果该值 >=8192 个段落,那么我们有足够的内存,我们可以随意访问它。如果我们确实有足够的内存,我们可以使用 Int 21h/AH=4ah
. We don't need to resize the memory DOS already allocated for us but it can be useful if your code needs to load/run a child program with DOS's Exec function Int 21h/AH=4bh
.
注意: DOS < 2.0 不支持 Memory Control Blocks,这意味着分配、释放和调整大小的 Int 21h
函数不可用。在 DOS < 2.0 上调用它们将无提示地失败。当调整大小时减少程序在内存中的大小时,该函数不应失败,因此我们应该能够忽略任何错误。
一个使用 GNU 汇编程序的程序版本,确保我们在堆栈之后有 128KiB 的空闲空间 space 可能如下所示:
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 , %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 [=11=]x4a, %ah # Request DOS resize our program's memory block
int [=11=]x21 # 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 , %ah
int [=11=]x21
.exit:
ret # We're done
no_mem_str: .asciz "Out of memory\n\r$"
_end:
一个稍微复杂一点的变体是将我们默认给定的堆栈大小调整为适合我们工作的大小,然后将 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 [=12=]x4a, %ah # Request DOS resize our programs memory block
int [=12=]x21 # 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 , %ah
int [=12=]x21
.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
这些示例可以组装并链接到名为 myprog.com
的程序,其中:
as --32 myprog.s -o myprog.o
ld -melf_i386 -Ttext=0x100 --oformat=binary myprog.o -o myprog.com
在 DOS EXE 程序中分配 128KiB
DOS 加载程序也加载 EXE 程序(它们有一个 MZ header)。 MZ header 包含程序信息、重定位表、堆栈、入口点以及超出可执行文件中实际存在的数据的最小和最大内存分配要求。完全未初始化数据的段(包括但不限于BSS和Stack段)在可执行文件中不占用space,但DOS加载器被告知通过MINALLOC[=分配额外的内存154=] 和 MAXALLOC header 字段:
MINALLOC. This word indicates the minimum number of paragraphs the program requires to begin execution. This is in addition to the memory required to hold the load module. This value normally represents the total size of any uninitialised data and/or stack segments that are linked at the end of a program. This space is not directly included in the load module, since there are no particular initialising values and it would simply waste disk space.
MAXALLOC. This word indicates the maximum number of paragraphs that the program would like allocated to it before it begins execution. This indicates additional memory over and above that required by the load module and the value specified by MINALLOC. If the request cannot be satisfied, the program is allocated as much memory as is available
MINALLOC 是 EXE 本身 需要 的代码和数据之上的段落数。 MAXALLOC 始终至少等于 MINALLOC,但如果 (MAXALLOC > MINALLOC),则 DOS 将尝试满足附加段落的请求 (MAXALLOC-MINALLOC)。如果不能满足该请求,那么 DOS 将分配它确实拥有的所有可用 space。通常,MAXALLOC 和 MINALLOC 之间的额外内存被许多工具和编程语言称为 HEAP。
值得注意的是,它是生成设置MINALLOC 和MAXALLOC 的可执行文件的最终链接过程。默认情况下,链接器通常将 MAXALLOC 设置为 0xffff,有效地请求 HEAP 占用 DOS 可以分配的尽可能多的连续 space。 EXEMOD
程序旨在允许更改此内容:
EXEMOD
EXEMOD 显示或更改 DOS 文件中的字段 header。使用 此实用程序,您必须了解文件 header
的 DOS 约定[snip]
/MIN n Sets the minimum allocation value to n, where n is a hexadecimal value setting the number of paragraphs. The actual value set may be different from the requested value if adjustments are necessary to accommodate the stack.
/MAX n
Sets the maximum allocation to n, where n is a hexadecimal value setting the number of paragraphs. The maximum allocations value must be greater than or equal to the minimum allocation value. This option has the same effect as the linker parameter ICPARMAXALLOC.
在没有内存控制块概念的 DOS < 2.0 中,使用 EXEMOD
是更改 DOS 可执行文件的额外内存要求的方法。在 DOS 2.0+ 中,程序(位于 run-time)可以通过 DOS Int 21h
函数分配新内存、调整内存大小和释放内存。
对于此讨论,程序 需要 128KiB 的额外内存,因此示例将把该数据放在未初始化的数据中。 linking/executable 生成过程将通过添加所需的额外段落来调整 MZ header 中的 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
适用于大多数 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
脚注:
- 1当 DOS 剩余可用内存少于 64KiB 时,SP 将被设置为从偏移量向下增长在 DOS 可用空闲内存的顶部下方。当有 64KiB 或更多可用内存时,DOS 加载程序将 SP 设置为 0x0000。在 >= 64KiB 可用内存的情况下,第一次推送数据(return 地址 0x0000)将 SP 包装到 0xfffe(0x0000-2)段的顶部).这是一个实模式的怪癖:如果你设置 SS:SP 到 SS:0x0000 推送的第一个值将被放置在SS:0xFFFE 在 SS 段的顶部。
- 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 之间的未使用区域。