我最近偶然发现了这个问题。尽管它只有几年的历史,但我觉得除了目前的答案之外,还有一些其他信息可能对未来的读者有用。
这个问题实际上可以归结为:我可以任意写入超出DOS分配的程序范围的内存吗?这个问题是针对DOS COM程序的,但大部分信息也适用于DOS EXE程序。
GNU汇编程序的局限性在于它不能生成16位DOS EXE程序,所以您必须生成DOS COM程序。原点为0x100的DOS COM程序。代码、数据和堆栈不能超过64KiB内存(在加载时)。DOS COM程序一旦被DOS加载程序加载到内存中,就具有以下特征:
-
进入时
DS=ES=SS=CS
.
-
该程序可重定位到任何段,并且不包含加载时修正/重定位。
-
即使加载DOS COM程序时限制为<=,程序也会从DOS内存池中分配最大的连续可用块64KiB内存。DOS加载程序有效地将整个空闲池分配给COM程序。
-
DOS加载程序始终设置SS=CS,但
服务提供商
可以从0x0000以外的值开始
1.
如果我们程序的可用空间少于64KiB。
-
DOS加载程序总是在将控制权转移到
CS:0x0100
开始我们的计划。CS:0x0000是PSP的开始,PSP以2字节指令(0xcd 0x20)开始
Int 20h
.
国际20小时
终止当前程序。这是允许DOS COM程序执行
ret
以终止程序。
-
有一个程序控制块,称为
Program Segment Prefix
(PSP)DOS在内存中放置在CS:0x0000和CS:0x0100之间
-
COM程序在CS处开始执行:0x0100
人们应该问的第一个问题是:我的DOS COM程序实际上有多少内存?简单的答案是:它会有所不同。它可能因可用的常规内存量而异(IBM PC通常带有64KiB、128KiB、256KiB、512KiB或640KiB)。另一个答案中引用的Dobbs博士杂志文章发表于1988年,记忆图谱缺少一些关键的东西。
1987年,IBM发布了IBM PS/2系列计算机。为了保存鼠标相关信息,IBM意识到
BIOS Data Area
在中断向量表上方,因此他们创建了一个
Extended BIOS Data Area
(EBDA)。此内存由BIOS保留,IBM PS/2 BIOS开始报告1KiB更少的内存(639KiB而不是640KiB)。EBDA可以有不同的大小,具体取决于BIOS制造商。BIOS
Int 12h
call将返回不包括EBDA区域的常规内存量(<=640KiB)。DOS依赖于此来确定它有多少可用内存。
更糟糕的是,当基于386SL的系统发布时,它包括
System Management Mode
它在第二环上运行,可以完全访问您的PC。这些系统也开始使用EBDA中的空间。某些系统需要超过1KiB。理论上,你可以拥有128KiB的EBDA空间,尽管我不确定是否有任何系统有过这样的空间!此区域最终用于电源管理(APM)、ACPI、SMBIOS,并且可以随时通过系统管理模式写入此区域。出于这个原因,该区域通常被视为OS保留的区域。实际发生的情况取决于BIOS和机器制造商。
除了EBDA,一些DOS程序(和恶意软件)拦截BIOS Int 12h并报告较少内存,以隐藏(或驻留)DOS不应该接触的代码/数据。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,特别是偏移量处的WORD值来回答
CS:0x0002
:
02h-03h字(2字节)分配给程序的内存之外的第一个字节的段
通过读取这个值,你可以得到第一个字节的段,刚好超出你的程序被分配的部分(我们称之为
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和堆栈正上方的段限定(
服务提供商
)正在指向。由于DOS总是推送一个2字节的值(返回地址
ret(雷特)
将返回到)对于COM程序-下一段可以通过除以
服务提供商
乘以16(或SHR乘以4),再加上1(我们称之为
SEGAFTERSTACK
).
最简单的方法是将128KiB的数据放在堆栈的上边缘(
SEGAFTERSTACK公司
). 我们只需要确保
SEGAFTERSTACK公司
和
NEXTSEG公司
(DOS给我们的程序区域的范围)。如果该值为>=8192段,那么我们有足够的内存,我们可以自由访问它,因为我们认为合适。如果我们有足够的内存,我们可以要求DOS将COM程序的大小调整到所需的确切空间
Int 21h/AH=4ah
。我们不需要调整已经分配给我们的DOS内存的大小,但如果您的代码需要加载/运行带有DOS的Exec函数的子程序,这将非常有用
Int 21h/AH=4bh
.
注:
DOS<2.0不支持
Memory Control Blocks
这意味着
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:
一个稍微复杂的变体是将默认给定的堆栈大小调整为适合我们工作的大小,然后将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
这些示例可以组装并链接到名为
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头包含程序信息、重定位表、堆栈、入口点以及可执行文件中物理存在的数据之外的最小和最大内存分配要求。具有完全未初始化数据的段(包括但不限于BSS和堆栈段)不会占用可执行文件中的空间,但DOS加载程序被告知通过
MINALLOC公司
和
MAXALLOC公司
标题字段:
MINALLOC公司
.
该词表示程序开始执行所需的最少段落数。这是对内存的补充
需要固定负载模块。该值通常表示
任何未初始化数据和/或堆栈段的总大小
在程序末尾链接。此空间不直接包含在
加载模块,因为没有特定的初始化值和
这只会浪费磁盘空间。
MAXALLOC公司
。该词表示
程序要分配的最大段落数
在它开始执行之前。这表示有额外的内存
高于加载模块和值所需的值
由MINALLOC规定。如果无法满足请求,程序
分配了尽可能多的可用内存
MINALLOC是EXE本身代码和数据上方的段落数
必修的
.MAXALLOC始终至少等于MINALLOC,但如果是(MAXALLOC>MINALLOC),则DOS将尝试满足附加段落的请求(MAXALOC-MINALLOC)。如果无法满足该请求,DOS将分配它所拥有的所有可用空间。通常,MAXALLOC和MINALLOC之间的额外内存称为
HEAP(堆)
通过许多工具和编程语言。
值得注意的是,生成设置MINALLOC和MAXALLOC的可执行文件的是最终链接过程。通常,链接器默认情况下会将MAXALLOC设置为0xffff,从而有效地请求HEAP占用DOS可以分配的尽可能多的连续空间。这个
EXEMOD
该程序旨在允许更改:
EXEMOD公司
EXEMOD显示或更改DOS文件头中的字段。要使用
这个实用程序,您必须了解文件头的DOS约定
[剪]
/最小n
将最小分配值设置为n,其中n是
设置段落数的十六进制值。这个
实际值集可能与请求值不同
如果需要调整以适应堆栈。
/最大n
将最大分配设置为n,其中n是
设置段落数的十六进制值。这个
最大分配值必须大于或等于
到最小分配值。此选项具有
与链接器参数ICPARMAXALLOC的效果相同。
在DOS<中;2.0没有内存控制块的概念,使用
EXEMOD公司
是更改DOS可执行文件的额外内存要求的方法。在DOS 2.0+中,程序(在运行时)可以通过DOS分配新内存、调整内存大小和释放内存
国际21小时
功能。
对于此讨论,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
适用于大多数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时,
服务提供商
将设置为从DOS可用可用内存顶部以下的偏移量开始向下增长。当有64KiB或更多可用内存时,DOS加载程序设置
服务提供商
到0x0000。在>=的情况下64KiB可用的可用内存第一次推送数据(返回地址0x0000)
服务提供商
到0xfffe处的段顶部(0x0000-2)。这是一个真正的模式怪癖:如果您设置
不锈钢:SP
对于SS:0x0000,推送的第一个值将放置在
不锈钢
段
-
2.
虽然
0xa000:0x0000个
通常被视为DOS可用的连续常规内存的上端,它不一定非得这样。一些内存管理器(JEMMEX、QEMM、386Max等)及其工具可以成功地移动EBDA(在不会导致问题的设备上),并且可以得知0xa000:0x0000到0xa000:00 xffff的VGA/EGA内存未使用,可以将DOS分配的连续内存的上端移动到0xb000:0x000。在无头(无视频)配置中,甚至可以拥有更多。这样做的386内存管理器通常在v8086模式下运行DOS,并将扩展内存(使用386对分页的支持)重新映射到0xa000:0x0000和0xf000:0xffff之间的未使用区域。