我想写一个小的DOS程序(我的第一个),但我有点缺乏经验。
对于这个程序,我需要超过64千字节的(传统的)内存。如何获得额外的内存?理想情况下,我希望为程序获得两个额外的64k内存块。我可以直接开始将数据写入地址空间的某个位置吗?还是需要请求额外的内存?
我想写一个小的DOS程序(我的第一个),但我有点缺乏经验。
对于这个程序,我需要超过64千字节的(传统的)内存。如何获得额外的内存?理想情况下,我希望为程序获得两个额外的64k内存块。我可以直接开始将数据写入地址空间的某个位置吗?还是需要请求额外的内存?
我最近偶然发现了这个问题。虽然它已经几年了,但我觉得除了当前答案之外,一些额外的信息可能对未来的读者有用。
Int 20h
开始。 Int 20h
终止当前程序。这是允许DOS COM程序使用ret
来终止程序的机制。1987年IBM发布了IBM PS/2系列电脑。为了保存鼠标相关信息,IBM意识到位于中断向量表上面的BIOS数据区没有足够的空间,因此他们创建了一个扩展BIOS数据区(EBDA)。这个内存由BIOS保留,而IBM PS/2 BIOS开始报告比实际少1KiB的内存(639KiB而不是640KiB)。EBDA的大小取决于BIOS制造商。BIOS Int 12h
调用将返回不包括EBDA区域在内的常规内存(<=640KiB)的数量。DOS依靠这个来确定可用的内存量。
故事寓意: 你不应该假设可用于你的内存量在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
处的WORD值:
通过读取此值,您可以获得程序分配的第一个字节之后的段(我们称其为02h-03h word (2 bytes) 分配给程序的内存后面第一个字节所在的段
NEXTSEG
)。通常,NEXTSEG
将是0xA000或0x9FC0(具有1KiB EBDA的系统将具有此值)。由于之前讨论的原因,它会在硬件上有所不同。该区域将重叠MS-DOS的COMMAND.COM的瞬态部分。实际上,我们加载后可以保证独占的内存区域是我们可以自由使用CS:0x0000
和NEXTSEG:0x0000
之间的所有物理内存。
由于20位段偏移地址的重叠性,每个段指向内存中一个不同的16字节区域的起始位置,称为段落。将段增加1会在内存中前进16字节,而将其减少则会后退16字节。这在计算所需内存量并确保有足够的内存来满足请求时非常重要。
128KiB等于128*1024/16=8192个段落。我们的COM程序加载到的实际区域大小(以及堆栈放置的位置)由CS:0x0000和堆栈(SP)指向的段之间的边界限制。由于DOS总是为COM程序推送一个2字节值(ret
返回的返回地址),因此可以通过将SP除以16(或SHR除以4)并加1来计算下一个段落(我们将其称为SEGAFTERSTACK
)。
SEGAFTERSTACK
)。我们只需要确保在SEGAFTERSTACK
和NEXTSEG
(DOS分配给我们的程序区域的范围)之间有足够的空间。如果该值大于等于8192个段,则我们有足够的内存,可以自由地访问它。如果我们有足够的内存,我们可以使用Int 21h/AH=4ah
请求DOS将我们的COM程序调整为所需的确切空间大小。我们不需要调整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:
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程序(它们具有MZ头部)。MZ头包含程序信息、重定位表、堆栈、入口点以及除可执行文件中实际存在的数据外所需的最小和最大内存分配要求。完全未初始化数据的段(包括但不限于BSS和堆栈段)不占用可执行文件中的空间,但是DOS加载器通过MINALLOC和MAXALLOC头字段被告知要分配额外的内存:
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具有相同的效果。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
.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
0xa000:0x0000
通常被视为DOS可用的连续常规内存的上限,但不一定非得如此。一些内存管理器(例如JEMMEX、QEMM、386Max等)及其工具可以成功移动EBDA(在设备上不会引起问题的情况下),并且可以告诉系统VGA/EGA内存在0xa000:0x0000至0xa000:0xffff之间未使用,可以将DOS分配的连续内存的上限移动到0xb000:0x0000。即使在没有视频的无头(headless)配置中,也可以有更多的内存。执行此操作的386内存管理器通常在v8086模式下运行DOS,并将扩展内存(使用386对分页的支持)重新映射到0xa000:0x0000和0xf000:0xffff之间未使用的区域。FFFE
。 - fuzAddress (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
0A_0000h
以下的内存并不总是可用的。其他程序可以使用低内存区域的“顶部”内存。EBDA也可以放置在那里。即使如此,如果将UMB分配给DOS的内存链(假设没有其他用户),则在段9FFFh处,第一个UMCB将使用一个段落。此外,使用loadhigh
,代码段甚至可以从0A_0000h
以上开始。最好使用int 21.4A将应用程序的内存块缩小为64 KiB,然后使用21.48分配块。 - ecm如果我们启动一个程序,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
cs = ds = es = ss
,所以第一个片段实际上是必需的吗? - fuz.COM
程序中不行,因为.COM
程序默认只有一个段。只有.EXE
程序可以有多个段。所以.COM
被限制在64kb内。 - zx485通过将段寄存器之一设置为所需值,您可以获取任何想要的段。但请记住: