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

时间:2018-03-23 14:15:26

标签: assembly memory x86 include nasm

我有两个nasm语法汇编文件,比方说a.asmb.asm
它们需要组合成两个单独的二进制文件a.binb.bin
启动时a.bin将由另一个程序加载到内存中的固定位置(0x1000b.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文件的标签,以便可以调用它们,包括最终汇编文件中的实际代码?

2 个答案:

答案 0 :(得分:3)

你可以这样继续:

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

    sym EQU addr
    

    其中addrsym

  5. 给出的nm的实际地址
  6. 在编译asyms.asm时包含或链接b.bin。这使得汇编代码可以看到a.bin中符号的地址,而无需提取相应的代码。
  7. 您要做的事情被称为构建叠加层。我相信一些汇编程序和链接器确实支持这类事情,但我不确定细节。

答案 1 :(得分:3)

你有很多可能性。这个答案主要关注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 Calls会带来一些开销,但它们更灵活,允许您更好地利用1MB以下的内存。通过FAR调用调用的函数必须使用FAR返回(retf)。使用从其他段传递的指针的远程函数通常需要处理段指针的偏移量(FAR指针),而不仅仅是偏移量。
  • 使用本答案中的方法:无论何时对公共库进行更改,都必须重新链接所有依赖它的程序,因为导出(公共)函数和数据的绝对内存地址可能会发生变化。