code/data 的引用符号分别加载到内存的另一部分

Referencing symbols of code/data loaded separately to another part of memory

我有两个 nasm-syntax 汇编文件,比方说 a.asmb.asm.
它们将需要组装成两个 单独的 二进制文件,a.binb.bin.
启动时 a.bin 将由另一个程序加载到内存中的固定位置 (0x1000)。
b.bin 稍后将加载到内存中的任意位置。
b.bin 将使用 a.bin.
中定义的一些函数 问题: b.bin 不知道函数在 a.bin

中的位置

为什么它们需要分开?它们是不相关的,将 b.bin(以及更多文件)和 a.bin 放在一个文件中会打败文件系统的目的。

为什么不用%include呢? 内存占用,a.bin 是相当大的一组函数占用了很多内存,而且因为640kb x86 实模式下的内存限制,我真的不能为每个需要它的文件都在内存中使用它。

可能的解决方案 1: 只需对位置进行硬编码。
问题: 如果我在 a.bin 开始时更改一些小的东西怎么办?我需要在它之后更新所有指向内容的指针,这并不方便。

可能的解决方案 2: 跟踪一个文件中的函数位置,然后 %include 那。
如果我没有其他选择,这可能就是我要做的。如果 nasm 可以生成易于解析的符号列表,我什至可以自动生成此文件,否则仍然需要太多工作。

可能的解决方案 3: 在内存中保留一个 table 函数所在的位置,而不是函数本身。这还有向后兼容性的额外好处,如果我决定更改 a.bin,所有使用它的东西都不必随之更改。
问题: 间接调用真的很慢并且占用大量磁盘space,尽管这确实是一个小问题。 table 也会在磁盘和内存中占用一些 space。
我的想法是稍后添加它,作为一个库或类似的东西。因此,与 a.bin 一起编译的所有内容都可以通过使用直接调用和单独编译的内容来更快地调用它,例如。应用程序可以使用 table 来更慢但更安全地访问 a.bin.

TLDR;
如何包含来自另一个 asm 文件的标签,以便可以调用它们 w/o 包括最终汇编文件中的实际代码?

您可以这样进行:

  1. Assemble 和 link a.bin 从地址 0x1000.
  2. 加载
  3. 使用 nm 实用程序(或类似工具)转储 a.bin
  4. 的符号 table
  5. 编写脚本将符号 table 转换为汇编文件 asyms.asm,其中包含 a.bin 中的每个符号一行

    sym EQU addr
    

    其中 addr 是由 nm

  6. 给出的 sym 的实际地址
  7. 编译时包含或linkasyms.asmb.bin。这使得 a.bin 中的符号地址对您的汇编代码可见,而无需拉入相应的代码。

您正在尝试做的是构建一个 覆盖层。 我相信一些汇编程序和 linker 确实支持这种事情,但我是不确定细节。

您有多种可能性。这个答案着重于1和2的混合。虽然你可以创建一个table的函数指针,但我们可以通过符号名直接调用公共库中的例程,而无需将公共库例程复制到每个程序中。我使用的方法是利用 LD 和链接器脚本的强大功能来创建一个共享库,该共享库将在内存中具有一个静态位置,该位置可通过 FAR CALL(段和偏移形式函数地址)从其他地方加载的独立程序进行访问在 RAM 中。

大多数人在开始时都会创建一个链接描述文件,该脚本会在输出中生成所有输入部分的副本。可以创建在输出文件中永远不会出现(未加载)的输出节,但链接器仍然可以使用那些未加载节的符号来解析符号地址。

我创建了一个简单的通用库,其中包含 print_bannerprint_string 函数,使用 BIOS 函数打印到控制台。假定两者都是通过来自其他段的 FAR CALL 调用的。您可能将公共库加载到 0x0100:0x0000(物理地址 0x01000),但从其他段中的代码调用,例如 0x2000:0x0000(物理地址 0x20000)。示例 commlib.asm 文件可能如下所示:

bits 16

extern __COMMONSEG
global print_string
global print_banner
global _startcomm

section .text

; Function: print_string
;           Display a string to the console on specified display page
; Type:     FAR
;
; Inputs:   ES:SI = Offset of address to print
;           BL = Display page
; Clobbers: AX, SI
; Return:   Nothing

print_string:               ; Routine: output string in SI to screen
    mov ah, 0x0e            ; BIOS tty Print
    jmp .getch
.repeat:
    int 0x10                ; print character
.getch:
    mov al, [es:si]         ; Get character from string
    inc si                  ; Advance pointer to next character
    test al,al              ; Have we reached end of string?
    jnz .repeat             ;     if not process next character
.end:
    retf                    ; Important: Far return

; Function: print_banner
;           Display a banner to the console to specified display page
; Type:     FAR
; Inputs:   BL = Display page
; Clobbers: AX, SI
; Return:   Nothing

print_banner:
    push es                 ; Save ES
    push cs
    pop es                  ; ES = CS
    mov si, bannermsg       ; SI = STring to print
                            ; Far call to print_string
    call __COMMONSEG:print_string
    pop es                  ; Restore ES
    retf                    ; Important: Far return

_startcomm:                 ; Keep linker quiet by defining this

section .data
bannermsg: db "Welcome to this Library!", 13, 10, 0

我们需要一个链接描述文件,它允许我们创建一个我们最终可以加载到内存中的文件。此代码假定库将加载的段是 0x0100 和偏移量 0x0000(物理地址 0x01000):

commlib.ld

OUTPUT_FORMAT("elf32-i386");
ENTRY(_startcomm);

/* Common Library at 0x0100:0x0000 = physical address 0x1000 */
__COMMONSEG    = 0x0100;
__COMMONOFFSET = 0x0000;

SECTIONS
{
    . = __COMMONOFFSET;

    /* Code and data for common library at VMA = __COMMONOFFSET */
    .commlib  : SUBALIGN(4) {
        *(.text)
        *(.rodata*)
        *(.data)
        *(.bss)
    }

    /* Remove unnecessary sections */
    /DISCARD/ : {
        *(.eh_frame);
        *(.comment);
    }
}

非常简单,它有效地链接了一个文件 commlib.o,以便它最终可以在 0x0100:0x0000 加载。作为使用该库的示例程序可能如下所示:

prog.asm:

extern __COMMONSEG
extern print_banner
extern print_string
global _start

bits 16

section .text
_start:
    mov ax, cs                   ; DS=ES=CS
    mov ds, ax
    mov es, ax
    mov ss, ax                   ; SS:SP=CS:0x0000
    xor sp, sp

    xor bx, bx                   ; BL =  page 0 to display on
    call __COMMONSEG:print_banner; FAR Call
    mov si, mymsg                ; String to display ES:SI
    call __COMMONSEG:print_string; FAR Call

    cli
.endloop:
    hlt
    jmp .endloop

section .data
mymsg: db "Printing my own text!", 13, 10, 0

现在的诀窍是制作一个链接器脚本,它可以采用这样的程序并引用我们公共库中的符号,而无需再次实际添加公共库代码。这可以通过在链接描述文件的输出部分使用 NOLOAD 类型来实现。

prog.ld:

OUTPUT_FORMAT("elf32-i386");
ENTRY(_start);

__PROGOFFSET   = 0x0000;

/* Load the commlib.elf file to access all its symbols */
INPUT(commlib.elf)

SECTIONS
{
    /* NOLOAD type prevents the actual code from being loaded into memory
       which means if you create a BINARY file from this, this section will
       not appear */
    . = __COMMONOFFSET;
    .commlib (NOLOAD) : {
        commlib.elf(.commlib);
    }

    /* Code and data for program at VMA = __PROGOFFSET */
    . = __PROGOFFSET;
    .prog : SUBALIGN(4) {
        *(.text)
        *(.rodata*)
        *(.data)
        *(.bss)
    }

    /* Remove unnecessary sections */
    /DISCARD/ : {
        *(.eh_frame);
        *(.comment);
    }
}

公共库的 ELF 文件由链接器加载,.commlib 部分标记为 (NOLOAD) 类型。这将防止最终程序包含公共库函数和数据,但仍允许我们引用符号地址。

可以创建一个简单的测试工具作为引导加载程序。引导加载程序会将公共库加载到0x0100:0x0000(物理地址0x01000),而使用它们的程序加载到0x2000:0x0000(物理地址0x20000)。程序地址是任意的,我只是选择它,因为它在1MB以下的空闲内存中。

boot.asm:

org 0x7c00
bits 16

start:
    ; DL = boot drive number from BIOS

    ; Set up stack and segment registers
    xor ax, ax               ; DS = 0x0000
    mov ds, ax
    mov ss, ax               ; SS:SP=0x0000:0x7c00 below bootloader
    mov sp, 0x7c00
    cld                      ; Set direction flag forward for String instructions

    ; Reset drive
    xor ax, ax
    int 0x13

    ; Read 2nd sector (commlib.bin) to 0x0100:0x0000 = phys addr 0x01000
    mov ah, 0x02             ; Drive READ subfunction
    mov al, 0x01             ; Read one sector
    mov bx, 0x0100
    mov es, bx               ; ES=0x0100
    xor bx, bx               ; ES:BS = 0x0100:0x0000 = phys adress 0x01000
    mov cx, 0x0002           ; CH = Cylinder = 0, CL = Sector # = 2
    xor dh, dh               ; DH = Head = 0
    int 0x13

    ; Read 3rd sector (prog.bin) to 0x2000:0x0000 = phys addr 0x20000
    mov ah, 0x02             ; Drive READ subfunction
    mov al, 0x01             ; Read one sector
    mov bx, 0x2000
    mov es, bx               ; ES=0x2000
    xor bx, bx               ; ES:BS = 0x2000:0x0000 = phys adress 0x20000
    mov cx, 0x0003           ; CH = Cylinder = 0, CL = Sector # = 2
    xor dh, dh               ; DH = Head = 0
    int 0x13

    ; Jump to the entry point of our program
    jmp 0x2000:0x0000

    times 510-($-$$) db 0
    dw 0xaa55

引导加载程序将公共库(扇区 1)和程序(扇区 2)加载到内存后,它跳转到程序的入口点 0x2000:0x0000。


把它们放在一起

我们可以创建文件 commlib.bin

nasm -f elf32 commlib.asm -o commlib.o
ld -melf_i386 -nostdlib -nostartfiles -T commlib.ld -o commlib.elf commlib.o
objcopy -O binary commlib.elf commlib.bin

commlib.elf 也创建为中间文件。您可以使用以下方法创建 prog.bin

nasm -f elf32 prog.asm -o prog.o
ld -melf_i386 -nostdlib -nostartfiles -T prog.ld -o prog.elf prog.o
objcopy -O binary prog.elf prog.bin

创建引导加载程序 (boot.bin):

nasm -f bin boot.asm -o boot.bin

我们可以构建一个看起来像 1.44MB 软盘的磁盘映像 (disk.img):

dd if=/dev/zero of=disk.img bs=1024 count=1440
dd if=boot.bin of=disk.img bs=512 seek=0 conv=notrunc
dd if=commlib.bin of=disk.img bs=512 seek=1 conv=notrunc
dd if=prog.bin of=disk.img bs=512 seek=2 conv=notrunc

这个简单的例子可以适合单个扇区中的公共库和程序。我还在磁盘上硬编码了它们的位置。这只是一个概念证明,并不代表您的最终代码。

当我在 QEMU 中 运行 使用 qemu-system-i386 -fda disk.img 这个(BOCHS 也可以工作)时,我得到这个输出:


正在查看prog.bin

在上面的示例中,我们创建了一个 prog.bin 文件,该文件不应该包含公共库代码,但解析了符号。那是怎么回事?如果您使用 NDISASM,您可以将二进制文件反汇编为原点为 0x0000 的 16 位代码,以查看生成的内容。使用 ndisasm -o 0x0000 -b16 prog.bin 你应该看到这样的东西:

; Text Section
00000000  8CC8              mov ax,cs
00000002  8ED8              mov ds,ax
00000004  8EC0              mov es,ax
00000006  8ED0              mov ss,ax
00000008  31E4              xor sp,sp
0000000A  31DB              xor bx,bx
; Both the calls are to the function in the common library that are loaded 
; in a different segment at 0x0100. The linker was able to resolve these
; locations for us.
0000000C  9A14000001        call word 0x100:0x11  ; FAR Call print_banner
00000011  BE2000            mov si,0x20
00000014  9A00000001        call word 0x100:0x0   ; FAR Call print_string
00000019  FA                cli
0000001A  F4                hlt
0000001B  EBFD              jmp short 0x1a        ; Infinite loop
0000001D  6690              xchg eax,eax
0000001F  90                nop
; Data section
; String 'Printing my own text!', 13, 10, 0
00000020  50                push ax
00000021  7269              jc 0x8c
00000023  6E                outsb
00000024  7469              jz 0x8f
00000026  6E                outsb
00000027  67206D79          and [ebp+0x79],ch
0000002B  206F77            and [bx+0x77],ch
0000002E  6E                outsb
0000002F  207465            and [si+0x65],dh
00000032  7874              js 0xa8
00000034  210D              and [di],cx
00000036  0A00              or al,[bx+si]

我已经用一些评论对其进行了注释。


备注

  • 是否需要使用 FAR 调用?不,但如果您不这样做,那么您的所有代码都必须适合单个段,并且偏移量将无法重叠。使用 FAR 调用会带来一些开销,但它们更灵活,可以让您更好地利用 1MB 以下的内存。通过 FAR 调用调用的函数必须使用 FAR Returns (retf)。使用其他段传递的指针的远函数一般需要处理段指针的偏移量(FAR指针),而不仅仅是偏移量
  • 使用这个答案中的方法:任何时候你对公共库进行更改,你必须 re-link 所有依赖它的程序,作为导出的绝对内存地址(public ) 功能和数据可能会发生变化。