代码之家  ›  专栏  ›  技术社区  ›  fuz

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

  •  6
  • fuz  · 技术社区  · 8 年前

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

    对于这个程序,我需要64KB以上的(常规)内存。我怎样才能获得额外的内存?理想情况下,我想为该程序额外增加两块64k内存。我可以开始将数据写入地址空间吗?还是需要额外的内存?

    4 回复  |  直到 5 年前
        1
  •  8
  •   David Hoelzer    8 年前

    在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
    

    “官方”内存分配机制是通过内存控制块(MCB)和DOS中断0x21使用0x48分配和0x49释放内存。关于这一点,可以在下面找到很好的讨论 Microsoft support document .

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

        2
  •  7
  •   ecm    4 年前

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


    这个问题实际上可以归结为:我可以任意写入超出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之间的未使用区域。
        3
  •  5
  •   Dirk Wolfgang Glomp    8 年前

    如果我们启动一个程序,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
    
        4
  •  0
  •   Community SushiHangover    7 年前

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

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