汇编程序如何计算符号地址的段和偏移量?

How does assembler compute segment and offset for symbol addresses?

我学过编译器和汇编语言,所以我想自己写一个汇编程序作为练习。但是我有一些问题;

如何计算@DATA 或 OFFSET/ADDR VarA 等段的地址?

以一个简单的汇编程序为例:

    .model small
    .stack 1024
    .data
          msg db 128 dup('A')
    .code
    start:
        mov ax,@data
        mov ax,ds
        mov dx, offset msg
                           ; DS:DX points at msg
        mov ah,4ch
        int 21h            ; exit program without using msg
    end

那么汇编程序如何计算@data段的段地址呢?

它如何知道将什么放入 mov dx, offset msg 的立即数?

assembler 不知道 @datamsg 将在内存中结束,因此在对象中生成称为重定位(或 "fixups")的元数据 ( .OBJ) 文件,允许 link 用户和操作系统填写正确的值。

让我们看看稍微不同的示例程序会发生什么:

.model small
.stack 1024
.data
    msg db 'Hello, World!,'$'
.code
start:
    mov ax,SEG msg
    mov ds,ax
    mov dx,OFFSET msg
    mov ah,09h
    int 21h              ; write string in DS:DX to stdout
    mov ah,4ch
    int 21h              ; exit(AL)
end start

组装此文件时,assembler 无法知道 linker 会将此示例程序定义的任何内容放在哪里。这对您来说可能很明显,但是 assembler 不能假设它看到的是一个完整的程序。 assembler 不知道你是否会 link 它与其他目标文件或库一起使用,这可能导致 linker 将 msg 放在除开始之外的其他地方数据段。

所以当这个示例程序将 assembled 写入目标文件时,assembler 会生成两个重定位记录。如果您使用 MASM assemble 文件,您可以在使用 /Fl 开关生成的列表文件中看到:

 ; listing of the .obj assembler output, before linking
 0000               start:
 0000  B8 ---- R            mov ax,SEG msg
 0003  8E D8                mov ds,ax
 0005  BA 0000 R            mov dx,OFFSET msg
 0008  B4 09                mov ah,09h

清单的机器代码列中操作数旁边的 R 表示它们具有引用它们的重定位。当 linker 从目标文件创建 MS-DOS 格式的可执行文件时,它将能够提供从 msg 的数据段开始的正确偏移量。该值是一个 link 时间常数,因此只有 .obj 而不是 .exe 需要重新定位。

但是 linker 将无法提供 msg 段(数据段)的位置,因为 linker 不知道 MS 在哪里-DOS 将可执行文件加载到内存中。 (与现代主流 OS 每个进程都有自己的虚拟地址 space 不同,实模式只有一个地址 space 程序必须与设备驱动程序和 TSR 共享,并且 OS本身。)

因此 linker 将在生成的可执行文件中放置一个重定位,告诉 MS-DOS 根据加载位置调整立即操作数。


请注意,您可能希望通过编写一个仅适用于完整程序且仅生成 .COM 可执行文件的程序来简化您的 assembler 写作练习。这样就不用担心搬家了。您的 assembler 将决定所有内容在 .COM 格式允许的单个段中的位置。请注意,由于 .COM 文件不支持段重定位,因此无法使用 mov ax,@datamov ax,SEG msg 等指令。相反,CS=DS=ES=SS 在程序启动时,其值由 OS 的程序加载器选择。 (并且该值在 assemble 时未知。)

How can I compute the address for segments such as @DATA or like OFFSET/ADDR VarA?

有2个案例:

a) 汇编器正在生成平面二进制文件或 executable 文件本身,不涉及链接器

b) 汇编器正在生成目标文件,稍后将发送给链接器

请注意,您可以混合使用。例如,在某些汇编程序中(例如 NASM),有创建临时部分的关键字(例如 absolute),并且通过在内部使用临时部分来支持结构(结构中的字段是到从地址零开始的临时部分)。

对于这两种情况;汇编程序将源代码转换为某种内部表示形式(例如,可能是 "instruction data, operand 1 data, operand 2 data, ..." 东西),其中“jmp foo”和“mov eax,bar/5+33”等指令的内部表示形式可以简化太多并且需要在符号 table.

中包含对符号的一些引用

对于符号 table 本身,每个条目都有一个符号名称(例如 "foo"),它在哪个部分,该部分内可能的最低偏移量以及该部分内可能的最高偏移量部分。当最低可能偏移量和最高可能偏移量匹配时,并且该部分具有已知地址,汇编程序可以用实际值替换内部表示中对该符号的引用。

请注意,在某些情况下,您直到稍后才能知道一条指令有多大(例如,对于 80x86;如果目标地址接近,“jmp foo”可能是一条 2 字节指令,但是如果目标地址不接近,则可能需要是 3 字节指令或 5 字节指令,并且在您了解 "foo" 将具有的值之前无法做出决定);当你不知道一条指令有多大时,你就无法知道同一节中后面出现的任何符号的偏移量。这就是为什么您最终希望符号具有最低可能偏移量和最高可能偏移量的原因 - 这样即使您不知道符号的实际偏移量,您仍然可以知道偏移量足够小或太大并且可以仍然确定指令的大小(并更好地了解该部分中后面符号的值)。

更具体地说;在组装时你想做多遍,每遍尝试将每条指令的中间表示转换为更多 specific/complete 版本,并尝试改进符号的最低可能偏移量和最高可能偏移值(这样你就有 more/better下一关可以使用的信息)。

当您完成 "multiple passes" 并且汇编器正在生成平面二进制文件并且不涉及链接器时;一切都将是已知的(包括部分的地址和部分内所有符号的偏移量,并且会将所有指令转换为实际字节)并且您可以生成最终文件。

当您完成 "multiple passes" 并且汇编程序正在生成目标文件时;有些东西是未知的(节的地址),有些东西是已知的(节内所有符号的偏移量,所有指令的大小);并且目标文件格式将为您提供一种方式来提供您不知道的事情的详细信息(例如,需要修复的事情的列表,以及链接器可以用来修复它们的信息),您可以从指令的中间表示和符号 table.

的左侧提供

请注意,在某些情况下,目标文件格式可能无法支持(例如,可能是之前的“mov eax,bar/5+33”),其中一条指令可以毫无问题地进行汇编(如果汇编器正在生成平面二进制文件)必须被视为错误(如果汇编器正在生成目标文件)。在尝试创建目标文件时,您会发现这些情况(并生成相应的错误消息)。

请注意,这一切都符合一个很好的“3 阶段”安排,其中 "front-end" 将 "plain text" 输入转换为中间表示,即 "middle-end"(多次传递)尽可能细化中间表示,"back-end" 生成一个文件。只有后端需要关心目标文件格式是什么。