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

时间:2016-05-29 11:22:48

标签: assembly memory-management x86-16 dos gas

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

对于程序,我需要超过64千字节的(常规)内存。我怎样才能获得额外的记忆?理想情况下,我希望为程序增加两个64k的内存块。我可以开始在地址空间的某处写入数据,还是需要额外的内存?

4 个答案:

答案 0 :(得分:4)

我最近偶然发现了这个问题。尽管有几年的历史了,但我认为当前答案之外的一些其他信息可能对将来的读者有用


这个问题实际上可以归结为:我可以在DOS分配的程序范围之外任意写入内存吗?这个问题是针对DOS COM程序的,但是很多信息也适用于DOS EXE程序。

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

  • 在条目 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 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)。另一个答案中引用的《多布斯博士杂志》文章发表于1988年,内存图缺少一些关键的东西。

1987年,IBM发布了IBM PS / 2系列计算机。为了保存与鼠标有关的信息,IBM意识到中断向量表上方的BIOS Data Area中没有足够的空间,因此他们创建了Extended BIOS Data Areas(EBDA)。此内存由BIOS保留,IBM PS / 2 BIOS开始报告少1KiB的内存(639KiB而不是640KiB)。 EBDA的大小可能有所不同,具体取决于BIOS制造商。 BIOS Int 12h调用将返回EBDA区域以外的常规内存量(<= 640KiB)。 DOS依靠此来确定它可以使用多少内存。

更糟糕的是,当基于386SL的系统发布时,它包含System Management Mode,其运行在-2环上,可以完全访问您的PC。这些系统也开始使用EBDA中的空间。一些系统需要超过1KiB。从理论上讲,您可以拥有128KiB的EBDA空间,尽管我不确定是否有很多系统具有该功能!该区域最终被用于电源管理(APM),ACPI,SMBIOS,并且该区域可以随时通过系统管理模式写入。因此,操作系统通常认为该区域是保留的。实际发生的情况取决于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:0x00000xa000:0x0000 2 之间。< / p>

要回答有关如何分辨程序的内存区域的问题,可以通过查看PSP来解决,尤其是偏移量CS:0x0002处的WORD值:

  

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

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


分配128KiB的COM程序

由于20-bit segment:offset addressing的重叠性质,一个段指向内存中另一个称为 paragraph 的16字节区域的开始。将段递增1会在内存中增加16个字节,而递减会返回16个字节。这对于执行所需的算术以找出我们的程序需要多少并确保有足够的内存来满足请求很重要。

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

最简单的方法是将我们的128KiB数据放置在堆栈(SEGAFTERSTACK)的最上方。我们只需要确保SEGAFTERSTACKNEXTSEG之间有足够的空间(DOS给我们的程序区域的范围)。如果该值是> = 8192段落,那么我们有足够的内存,我们可以自由地访问它。如果确实有足够的内存,我们可以要求DOS使用Int 21h/AH=4ah将COM程序的大小调整为所需的确切空间。我们不需要调整已经为我们分配的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和Stack段)不会在可执行文件中占用空间,但是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约定

     

[snip]

     

/ MIN n   将最小分配值设置为n,其中n是一个   设置段落数的十六进制值。的   实际值设置可能与请求值不同   如果需要进行调整以容纳纸叠。

     

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

在没有内存控制块概念的DOS <2.0中,使用EXEMOD是一种更改DOS可执行文件的附加内存要求的方法。在DOS 2.0+中,程序(在运行时)可以通过DOS Int 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

适用于大多数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,则第一次推送数据(返回地址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之间的未使用区域。

答案 1 :(得分:3)

在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

&#34;官方&#34;内存分配机制是通过内存控制块(MCB)和DOS中断0x21使用0x48来分配和0x49来释放内存。有关这方面的详细讨论可以在Microsoft support document中找到。

有关中断方法的文档,you might look here.

答案 2 :(得分:2)

如果我们启动程序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

答案 3 :(得分:0)

您可以通过将其中一个段寄存器设置为所需的值来获取所需的任何段。但请记住

  • 每个段以16字节边界开始,意味着0400(偏移量:0000)的段将等于0040的段(偏移量:3c00)和0000的另一个段(偏移量:4000)等等
  • 这些范围重叠,这意味着段寄存器中增加1会使绝对存储器地址增加16。
  • David Hoelzer
  • 在另一个答案中详细说明了BIOS或其他外围设备预设和使用的范围
  • 确保您的细分受众群大小为64kb且与其他细分不重叠