我有两个nasm语法汇编文件,比方说a.asm
和b.asm
。
它们需要组合成两个单独的二进制文件a.bin
和b.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文件的标签,以便可以调用它们,包括最终汇编文件中的实际代码?
答案 0 :(得分:3)
你可以这样继续:
a.bin
以从地址0x1000
加载。nm
实用程序(或类似工具)转储a.bin
编写一个脚本,将符号表转换为一个汇编文件asyms.asm
,其中包含a.bin
中每个符号的一行
sym EQU addr
其中addr
是sym
nm
的实际地址
asyms.asm
时包含或链接b.bin
。这使得汇编代码可以看到a.bin
中符号的地址,而无需提取相应的代码。您要做的事情被称为构建叠加层。我相信一些汇编程序和链接器确实支持这类事情,但我不确定细节。
答案 1 :(得分:3)
你有很多可能性。这个答案主要关注1和2的混合。虽然您可以创建函数指针表,但我们可以使用符号名称直接调用公共库中的例程,而无需将公共库例程复制到每个程序中。我使用的方法是利用LD和链接器脚本的强大功能来创建一个共享库,该库在内存中具有静态位置,可通过FAR CALL(段和偏移形式函数地址)从其他地方加载的独立程序访问在RAM中。
大多数人在开始时会创建一个链接描述文件,该脚本会在输出中生成所有输入节的副本。可以在输出文件中创建从不出现(未加载)的输出节,但链接器仍然可以使用这些非加载节的符号来解析符号地址。
我创建了一个简单的公共库,其中包含print_banner
和print_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也可以)中运行它时,我得到了这个输出:
在上面的示例中,我们创建了一个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]
我已经注释了一些评论。
retf
)。使用从其他段传递的指针的远程函数通常需要处理段和指针的偏移量(FAR指针),而不仅仅是偏移量。