assembly - 引用分别加载到内存另一部分的代码/数据的符号

标签 assembly memory x86 include nasm

我有两个nasm语法汇编文件,比如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是占用大量内存的大量函数,并且由于x86实模式下的640kb内存限制,我无法真正负担得起每个需要它的文件的内存容量。

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

可能的解决方案2:跟踪一个文件中的函数位置,并对其进行%include跟踪。
如果没有其他选择,这可能就是我会做的事情。如果nasm可以生成易于解析的符号列表,我甚至可以自动生成此文件,否则它仍然需要大量工作。

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

TLDR;
如何包含另一个asm文件中的标签,以便可以在最终的汇编文件中不包含实际代码的情况下调用它们?

最佳答案

您有很多可能性。这个答案集中在1和2的混合上。尽管您可以创建函数指针表,但是我们可以通过符号名称直接调用公共库中的例程,而无需将公共库例程复制到每个程序中。我使用的方法是利用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-system-i386 -fda disk.img在QEMU中运行它时(BOCHS也将运行),我得到以下输出:

enter image description here

看着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返回(retf)。使用从其他段传递的指针的远端函数通常需要处理指针的段和偏移量(FAR指针),而不仅仅是偏移量。
  • 在此答案中使用该方法:每次更改公共库时,都必须重新链接所有依赖该库的程序,因为导出(公共)函数和数据的绝对内存地址可能会发生变化。
  • 关于assembly - 引用分别加载到内存另一部分的代码/数据的符号,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49451675/

    相关文章:

    assembly - MIPS 中的延迟分支

    c++ - 如何使用 AVPacket 作为局部变量(或所说的临时变量)

    memory - 如何确定最大堆栈使用量?

    linux - CMP 命令无法正常工作

    c - Printf 参数未压入堆栈

    assembly - pcasm书籍示例

    c - 将数组作为参数从 C 传递给 x86 函数

    gcc - 内联汇编寻址方式

    assembly - NASM 错误 : invalid operands in non-64-bit mode

    c++ - 尝试编写自定义 allocate_shared 分配器并使其成为 thread_local 时崩溃