您有多种可能性。此答案侧重于 1 和 2 的混合。虽然您可以创建函数指针表,但我们可以通过符号名称直接调用公共库中的例程,而无需将公共库例程复制到每个程序中。我使用的方法是利用 LD 和链接器脚本的强大功能来创建一个共享库,该共享库将在内存中具有一个静态位置,通过 FAR CALL(段和偏移形式的函数地址)从其他地方加载的独立程序访问在内存中。
大多数人在开始时会创建一个链接描述文件,该脚本会在输出中生成所有输入部分的副本。可以创建在输出文件中从未出现(未加载)的输出段,但链接器仍可以使用这些未加载段的符号来解析符号地址。
我创建了一个简单的通用库,其中包含一个 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
在上面的示例中,我们创建了一个 prog.bin 文件,该文件中不包含公共库代码,但已解析了符号。是这样的吗?如果您使用 NDISASM,您可以将二进制文件反汇编为 16 位代码,起始点为 0x0000,以查看生成的内容。使用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]
我已经用几个 cmets 注释了它。
注意事项
- 是否需要使用 FAR 呼叫?不,但如果你不这样做,那么你的所有代码都必须放在一个段中,并且偏移量将无法重叠。使用 FAR 调用会带来一些开销,但它们更灵活,可以让您更好地利用 1MB 以下的内存。通过 FAR 调用调用的函数必须使用 FAR Returns (
retf)。使用从其他段传递的指针的 Far 函数通常需要处理段和指针的偏移量(FAR 指针),而不仅仅是偏移量。
- 使用此答案中的方法:无论何时更改公共库,都必须重新链接依赖它的所有程序,因为导出(公共)函数和数据的绝对内存地址可能会发生变化。