小工具搬运工

搬运一些实用的小工具

flat assembler g
介绍与概述

译文说明:本文档为同目录 fasmg.txt 的中文译本;代码示例保持原文。

目录

什么是 flat assembler g?
它是如何工作的?
解析指令参数有哪些手段?
标号是如何处理的?
如何并行生成文件的多个段?
解析其他种类语法有哪些选择?
如何控制符号标识符的识别上下文?
如何定义与核心伪指令同名的指令?
如何将宏指令转换为 CALM?

什么是 flat assembler g?

它是一种汇编引擎,设计为 flat assembler 1 所用引擎的继任者;后者是 x86 处理器上公认的汇编器之一。
这是一个裸引擎:本身不能识别和编码任何处理器的指令,但有能力成为任意 CPU 架构的汇编器。
它拥有相比 flat assembler 1 大幅改进的宏指令语言,允许以可定制宏指令的形式轻松实现指令编码器。
本工具的源码可用 flat assembler 1 编译,也可用 flat assembler g 自身编译。
源码中包含根据所用汇编器包含不同头文件的子句。当 flat assembler g 编译自身时,使用提供的头文件集,
实现与 flat assembler 1 语法大体兼容的 x86 指令与格式。
本包中 x86 架构的示例程序,是原先随 flat assembler 1 提供的精选样例;
它们使用实现指令编码器与输出格式化器的头文件集,像原版 flat assembler 一样汇编这些程序。
为演示如何实现不同架构的指令集,包内还有微控制器 8051 与 AVR 的示例程序。
它们保持简单,因而不提供针对此类 CPU 编程的完整框架,但可为创建此类环境提供坚实基础。
另有组装 JVM 字节码的示例,是对原先为 flat assembler 1 所做样例的移植。
因此它有些粗糙,未充分利用新引擎的能力,但有助于可视化 class 文件的结构。

它是如何工作的?

flat assembler g 的基本功能是根据源码中的指令生成输出。对于如下一行文本,
汇编器会生成一个具有所述值的单字节:

    db 90h

可定义宏指令,根据提供的参数生成特定数据序列。它们可对应所选机器语言的指令(如下例),
也可定义为生成其他用途的数据。

    macro int number
            if number = 3
                    db 0CCh
            else
                    db 0CDh, number
            end if
    end macro

    int 20h         ; generates two bytes        

如此看待的汇编可视为一种解释型语言,汇编器确实具有解释器的许多特征。
然而它也与编译器共享某些方面:指令可以使用在源码中更晚定义、且可能依赖该定义之前指令的值,如下例所示。

    macro jmpi target
            if target-($+2) < 80h & target-($+2) >= -80h                    
                    db 0EBh
                    db target-($+1)
            else
                    db 0E9h
                    dw target-($+2)
            end if 
    end macro

            jmpi start  
            db 'some data'  
    start:
    

上面定义的 "jmpi" 产生 8086 架构中的跳转指令代码。此类代码包含跳转目标的相对偏移,
存储在单字节或 16 位字中。相对偏移为目标地址与下一条指令地址之差。
特殊符号 "$" 提供当前指令的地址,用于计算相对偏移并判断是否能装入单字节。
因此上例中 "jmpi start" 生成的代码依赖标为 "start" 的地址值,而该地址又依赖
其前所有指令(含该跳转)的输出长度。这形成依赖环,汇编器需找到满足源码所创全部约束的解。
若汇编器只是命令式解释器,这将不可能。其语言因此在某些方面是声明式的。
为此类循环依赖求解难似解方程,甚至可构造 flat assembler g 确实能求解的例子:

    x = (x-1)*(x+2)/2-2*(x+1)
    db x

此处循环引用已归结为引用自身构造值的单一定义。flat assembler g 在此情况下能找到解,
但在许多其他情况下会失败。本汇编器所用方法是对源文多遍扫描,再凭所获知识预测所有值。
此法在机器码汇编中通常足够,但很少足以求解复杂方程;上例是例外之一。

解析指令参数有哪些手段?

并非所有指令都像前述例子那样语法简单。为处理可能含特殊构造的参数,
flat assembler g 提供若干有力工具,以下以实现 Z80 处理器部分指令为例演示。
所用特性的规则见手册。
当指令允许的参数组合很少时,可用 "match" 构造分别处理每一种:

    macro EX? first,second
            match (=SP?), first
                    match =HL?, second
                            db 0E3h
                    else match =IX?, second
                            db 0DDh,0E3h
                    else match =IY?, second
                            db 0FDh,0E3h
                    else
                            err "incorrect second argument"
                    end match
            else match =AF?, first
                    match =AF'?, second
                            db 08h
                    else
                            err "incorrect second argument"
                    end match
            else match =DE?, first
                    match =HL?, second
                            db 0EBh
                    else
                            err "incorrect second argument"
                    end match
            else
                    err "incorrect first argument"
            end match
    end macro

    EX (SP),HL
    EX (SP),IX
    EX AF,AF'
    EX DE,HL

许多地方的 "?" 字符将名称标为大小写不敏感;去掉这些 "?" 可进一步简化示例。
当参数可能取值较多但有规律时,可定义文本替换,将部分符号换为精心选择的、可被识别解析的构造:

    A? equ [:111b:]
    B? equ [:000b:]
    C? equ [:001b:]
    D? equ [:010b:]
    E? equ [:011b:]
    H? equ [:100b:]
    L? equ [:101b:]

    macro INC? argument
            match [:r:], argument
                    db 100b + r shl 3
            else match (=HL?), argument
                    db 34h
            else match (=IX?+d), argument
                    db 0DDh,34h,d
            else match (=IY?+d), argument
                    db 0FDh,34h,d
            else
                    err "incorrect argument"
            end match
    end macro

    INC A
    INC B
    INC (HL)
    INC (IX+2)

此法有一特点未必总合意:允许在参数中直接使用 "[:0:]" 这类表达式。
可在 "match" 中使用前缀防止此类语法被滥用:

    REG.A? equ [:111b:]
    REG.B? equ [:000b:]
    REG.C? equ [:001b:]
    REG.D? equ [:010b:]
    REG.E? equ [:011b:]
    REG.H? equ [:100b:]
    REG.L? equ [:101b:]

    macro INC? argument
            match [:r:], REG.argument
                    db 100b + r shl 3
            else match (=HL?), argument
                    db 34h
            else match (=IX?+d), argument
                    db 0DDh,34h,d
            else match (=IY?+d), argument
                    db 0FDh,34h,d
            else
                    err "incorrect argument"
            end match
    end macro

对形如 "(IX+d)" 的参数,有时希望允许代数等价形式如 "(d+IX)" 或 "(c+IX+d)"。
不必逐个解析每种变体,可让汇编器求值表达式,并将所选符号作特殊处理。
符号声明为 "element" 时无固定值,在表达式中代数地当作多项式中的变量项。

    element HL? 
    element IX? 
    element IY? 

    macro INC? argument
            match [:r:], REG.argument
                    db 100b + r shl 3
            else match (a), argument
                    if a eq HL
                            db 34h
                    else if a relativeto IX
                            db 0DDh,34h,a-IX
                    else if a relativeto IY
                            db 0FDh,34h,a-IY
                    else
                            err "incorrect argument"
                    end if
            else
                    err "incorrect argument"
            end match
    end macro

    INC (3*8+IX+1)

    virtual at IX
            x db ?
            y db ?
    end virtual        

    INC (y)

上述宏有一小问题:参数可含任意文本,放入表达式时可能导致异常行为。
例如处理 "INC (1|0)" 时,"a eq HL" 会变成 "1|0 eq HL",该逻辑表达式为真,尽管参数畸形。
这是宏指令简单文本替换的后果(最好改用 CALM 避免)。此处可用局部变量作代理保存参数值:

    macro INC? argument
            match [:r:], REG.argument
                    db 100b + r shl 3
            else match (a), argument
                    local value
                    value = a
                    if value eq HL
                            db 34h
                    else if value relativeto IX
                            db 0DDh,34h,a-IX
                    else if value relativeto IY
                            db 0FDh,34h,a-IY
                    else
                            err "incorrect argument"
                    end if
            else
                    err "incorrect argument"
            end match
    end macro

代理变量还有一优点:其值在宏开始生成任何输出之前计算。表达式含 "$" 时,
取值位置不同结果可能不同;代理变量确保取的是生成指令代码之前对参数求值的结果。
当表达式中允许的符号集较大时,最好用单一构造处理整族符号。
"element" 声明可为符号关联附加值,再用 "metadata" 算符从含该变量为项的线性多项式中提取。
下例是演示此特性的 INC 宏另一变体:

    element register
    element A? : register + 111b
    element B? : register + 000b
    element C? : register + 001b
    element D? : register + 010b
    element E? : register + 011b
    element H? : register + 100b
    element L? : register + 101b

    element HL?
    element IX? 
    element IY? 

    macro INC? argument
            local value
            match (a), argument
                    value = a
                    if value eq HL
                            db 34h
                    else if value relativeto IX
                            db 0DDh,34h,a-IX
                    else if value relativeto IY
                            db 0FDh,34h,a-IY
                    else
                            err "incorrect argument"
                    end if
            else match any more, argument
                    err "incorrect argument"
            else
                    value = argument
                    if value eq value element 1 & value metadata 1 relativeto register
                            db 100b + (value metadata 1 - register) shl 3
                    else
                            err "incorrect argument"
                    end if
            end match
    end macro

"any more" 模式用于捕获含多个 token 的复杂表达式参数,防止 "INC A+0" 或 "INC A+B-A"。
对某些指令集是否加入此约束可因人而异。
"value eq value element 1" 确保值除寄存器名外不含其他项。即使强制参数仅含单个 token,
仍可能有复杂值,如 "X = A + B" 或 "Y = 2 * A";"INC X" 与 "INC Y" 会使 "element 1" 返回 "A",
与两种情况下检查的值不同。
若指令接受可变数量参数,可用 "&" 修饰符将参数完整内容传给 "match":

    element CC
    
    NZ? := CC + 000b
    Z?  := CC + 001b
    NC? := CC + 010b
    C?  := CC + 011b
    PO  := CC + 100b
    PE  := CC + 101b
    P   := CC + 110b
    M   := CC + 111b   

    macro CALL? arguments&
            local cc,nn
            match condition =, target, arguments
                    cc = condition - CC
                    nn = target
                    db 0C4h + cc shl 3
            else
                    nn = arguments
                    db 0CDh                     
            end match
            dw nn
    end macro

    CALL 0
    CALL NC,2135h

此法还可处理参数含逗号或以不同方式分隔等更复杂情况。
CALM 宏能更精细控制参数解析,其 "match" 有更多选项。上宏可改写为:

    calminstruction CALL? target&
            local   condition
            match   condition:name =, target, target, :
            jno     unconditional
            check   defined condition & condition relativeto CC
            jno     error
            emit    1, 0C4h + (condition - CC) shl 3
            jump    address
        error:
            err     "unrecognized syntax"
        unconditional:
            emit    1, 0CDh
        address:
            emit    2, target
    end calminstruction

此次无需代理,因为参数文本仅在含它们的表达式求值时才被求值。
此外 "match" 可检查畸形表达式,就像检查条件符号名是否正确一样。

标号是如何处理的?

定义标号的标准方式是在名称后加 ":"(也起换行作用,同一行可跟任何其他命令,含另一标号)。
此类标号定义值等于当前地址的符号;初值为零,向输出添加字节时递增。
某些汇编语言变体希望标号可置于指令前而无需中间的 ":"。此时需创建标号宏,
在定义标号后将处理交给同名原宏指令:

    struc INC? argument
            .:
            INC argument
    end struc

    start   INC A
            INC B

需为每种需允许此语法的指令分别这样做。如下简单循环即可:

    iterate instruction, EX,INC,CALL
            struc instruction? argument
                    .: instruction argument
            end struc
    end iterate

每个生成数据的内建指令已有标号变体。
用 "?" 代替名称定义标号指令,可拦截以非已知指令标识符开头(因而假定为标号)的每一行。
下例允许源文中任意行以无 ":" 标号开头(亦处理标号后跟 ":" 或 "=" 及值的情况):

    struc ? tail&
            match :, tail 
                    .: 
            else match : instruction, tail
                    .: instruction
            else match == value, tail
                    . = value
            else 
                    .: tail
            end match 
    end struc

应用此类全局效果后,通常不必再为各指令定义标号宏。应按所需语法选择变体。
拦截以 ":" 定义的标号也有用:当前地址赋给标号前需额外处理——例如处理器地址单位大于字节时。
拦截宏可如下:

    struc ? tail&
            match :, tail 
                    label . at $ shr 1
            else match : instruction, tail
                    label . at $ shr 1
                    instruction
            else
                    . tail
            end match
    end struc

用于定义标号的当前地址可用 "org" 改变。若需将标号与绝对值区分,可用 "element" 符号构成地址:

    element CODEBASE
    org CODEBASE + 0

    macro CALL? argument
            local value
            value = argument
            if value relativeto CODEBASE
                    db 0CDh
                    dw value - CODEBASE
            else
                    err "incorrect argument"
            end if 
    end macro

要在不写入输出的地址空间中定义标号,应声明 "virtual" 块。下例准备 "DATA" 与 "CODE" 宏,
在生成程序指令与数据标号之间切换;仅指令码进入输出:

    element DATA
    DATA_OFFSET = 2000h
    element CODE
    CODE_OFFSET = 1000h

    macro DATA?
            _END
            virtual at DATA + DATA_OFFSET
    end macro

    macro CODE?
            _END
            org CODE + CODE_OFFSET
    end macro

    macro _END?
            if $ relativeto DATA
                    DATA_OFFSET = $ - DATA
                    end virtual
            else if $ relativeto CODE
                    CODE_OFFSET = $ - CODE
            end if
    end macro

    postpone
            _END
    end postpone

    CODE

此处用 "postpone" 确保 "virtual" 块总能正确关闭,即使源文以数据定义结束。
在上述环境中,任何指令都能区分数据标号与程序内标号。例如分支指令可接受程序内标号或绝对值,
但不接受数据标号:

    macro CALL? argument
            local value
            value = argument
            if value relativeto CODE
                    db 0CDh
                    dw value - CODE
            else if value relativeto 0
                    db 0CDh
                    dw value
            else
                    err "incorrect argument"
            end if 
    end macro

    DATA

    variable db ?

    CODE

    routine:

此上下文中 "CALL routine" 或 "CALL 1000h" 允许,"CALL variable" 不允许。
当标号值非绝对数时,可为使用它们的指令生成重定位。
可用特殊 "virtual" 块存储程序基址改变时需重定位的值在程序内的偏移:

    virtual at 0
            Relocations::
            rw RELOCATION_COUNT
    end virtual

    RELOCATION_INDEX = 0

    postpone
            RELOCATION_COUNT := RELOCATION_INDEX                
    end postpone

    macro WORD? value
            if value relativeto CODE
                    store $ - CODE : 2 at Relocations : RELOCATION_INDEX shl 1
                    RELOCATION_INDEX = RELOCATION_INDEX + 1
                    dw value - CODE
            else
                    dw value
            end if
    end macro 

    macro CALL? argument
            local value
            value = argument
            if value relativeto CODE | value relativeto 0
                    db 0CDh
                    word value
            else
                    err "incorrect argument"
            end if 
    end macro 

如此创建的重定位表可用 "load" 访问。下列两行可将整表放入输出某处:

    load RELOCATIONS : RELOCATION_COUNT shl 1 from Relocations : 0
    dw RELOCATIONS

"load" 将整表读入单字符串,"dw" 写入输出(填充到字倍数;此例字符串无需填充)。
更复杂重定位可能需要额外修饰符。例如地址高低部分须存于不同位置并分别重定位时,
可实现如下修饰符:

    element MOD.HIGH
    element MOD.LOW

    HIGH? equ MOD.HIGH +
    LOW? equ MOD.LOW +

    macro BYTE? value
            if value relativeto MOD.HIGH + CODE
                    ; register HIGH relocation
                    db (value - MOD.HIGH - CODE) shr 8
            else if value relativeto MOD.LOW + CODE
                    ; register LOW relocation
                    db (value - MOD.LOW - CODE) and 0FFh
            else if value relativeto MOD.HIGH
                    db (value - MOD.HIGH) shr 8
            else if value relativeto MOD.LOW
                    db (value - MOD.LOW) and 0FFh
            else
                    db value
            end if
    end macro 

为清晰省略了登记重定位的命令;此情况除代码内偏移外还需在相应结构中登记额外信息。
经此准备,代码中可生成:

    BYTE HIGH address
    BYTE LOW address

凡内部用 "byte" 宏生成代码的指令,都可借此轻松启用带修饰符的语法。

如何并行生成文件的多个段?

本引擎只有一个须顺序生成的主输出。当文件需含代码与数据等不同段,
且片段交错、散布于多个源文件时,这似乎成问题。但有若干方法处理,均或多或少依赖汇编器的前向引用能力。
自然做法是在 "virtual" 块中定义辅助段内容,再一次性复制到输出适当位置。
"virtual" 块带标号时可多次重开以追加数据。

            include '8086.inc'
            org     100h
            jmp     CodeSection

    DataSection:

            virtual
                    Data::
            end virtual

            postpone
                    virtual Data
                            load Data.OctetString : $ - $$ from $$
                    end virtual
            end postpone

            db Data.OctetString

    CodeSection:

            virtual Data
                    Hello db "Hello!",24h
            end virtual

            mov     ah,9
            mov     dx,Hello
            int     21h

            virtual Data
                    ExitCode db 37h
            end virtual

            mov     ah,4Ch
            mov     al,[ExitCode]
            int     21h

即使无额外宏,语法也相对简单。
另一法是将段片段放入宏,在源文所需位置全部执行。缺点是追踪定义中的错误可能较麻烦。
便于并行段追加的技术也有利于生成重定位表等数据结构。
可在重开的 "virtual" 块内用常规数据伪指令代替早前演示用的 "store" 创建重定位记录。

解析其他种类语法有哪些选择?

有时汇编器需解析的命令并非以指令名或标号开头。名称前可能有 "."、"!" 等特殊字符,
或完全是不同构造。此时需用 "macro ?" 拦截整行源文并处理此类特殊语法。
例如若需允许 ".CODE" 命令,无法直接实现为宏,因为首点使符号被解释为局部符号,
无法这样执行全局定义的指令。拦截宏提供解法:

    macro ? line&
            match .=CODE?, line
                    CODE
            else match .=DATA?, line
                    DATA
            else
                    line
            end match
    end macro  

含 ".CODE" 或 ".DATA" 的行在此被处理为调用对应名称的全局宏,其余拦截行不变执行。
此法可滤出特殊语法,让汇编器照常处理常规指令。
有时非传统语法仅出现在有边界的块内。解析宏应仅用于该处,块结束时用 "purge" 移除:

    macro concise
            macro ? line&
                    match =end =concise, line
                            purge ?
                    else match dest+==src, line
                            ADD dest,src
                    else match dest-==src, line
                            SUB dest,src
                    else match dest==src, line
                            LD dest,src
                    else match dest++, line
                            INC dest
                    else match dest--, line
                            DEC dest
                    else match any, line
                            err "syntax error"
                    end match
            end macro
    end macro

    concise
            C=0
            B++
            A+=2
    end concise

如此定义的宏不拦截控制汇编流的伪指令如 "if" 或 "repeat",块内仍可自由使用。
若声明为 "macro ?! line&",则会无例外拦截每一行。
另一选择是用 "struc ?" 仅拦截不以已知指令开头的行(首符号当作标号)。
因只测试未知命令,汇编开销应更小:

    struc (head) ? tail&
            match .=CODE?, head
                    CODE tail
            else
                    head tail
            end match
    end struc

这些方法都有隐蔽陷阱:以 ":" 定义的标号后可在同一行跟另一指令。
若下一指令(此处藏在 "tail" 中)是 "if" 等控制伪指令,放在 "else" 分支会破坏控制块嵌套。
解法是在 "match" 块外 somehow 调用 "tail"。一法是通过特殊宏:

    struc (head) ? tail&
            local invoker
            match .=CODE?, head
                    macro invoker
                            CODE tail
                    end macro
            else
                    macro invoker
                            head tail
                    end macro
            end match
            invoker
    end struc

更简单的是直接调用原行;需覆盖时,用另一行拦截器使该行被忽略(随即自我清除):

    struc (head) ? tail&
            match .=CODE?, head
                    CODE tail
                    macro ? line&
                            purge ?
                    end macro
            end match
            head tail
    end struc

更好的是用 CALM 代替标准宏:可在不用控制伪指令的情况下处理参数并汇编原行或修改行。
CALM 性能更好,对几乎每行都调用的拦截器尤为重要。

如何控制符号标识符的识别上下文?

设计通用宏时,须确保在任何调用处正确工作。"local" 可为宏的每次实例创建私有符号而不干扰未知环境,
但有时变量需在多次宏调用间共享。全局符号通常够用,但可能被干扰。考虑:

    GLOBAL_STATE = 0

    macro state 
            db GLOBAL_STATE
    end macro

    macro switch value
            GLOBAL_STATE = GLOBAL_STATE xor (value)
    end macro

此解法有问题:若别处也用 "GLOBAL_STATE" 会干扰这些宏。罕见特定全局名略有帮助
(多个全局最好用罕见名专用命名空间),但最好 somehow 隔离这些符号。
若在 "namespace" 块内使用上述宏,"GLOBAL_STATE" 的定义会在该命名空间创建符号。
第二问题有一基本解法:标识符末尾加点,确保定义的是与右侧访问相同的符号:

    GLOBAL_STATE. = GLOBAL_STATE xor (value)

还有另一选择。宏的参数为其文本中标识符保留上下文(除非参数名前加 "&" 明确不要)。
下例演示效果:

    macro tester arg
            namespace X
                    a = 0
                    db a
                    db arg
            end namespace
    end macro

    a = 3
    tester a

两条汇编的数据定义文本同为 "db a",但第二条在宏调用处的上下文解释 "a",生成字节 3 而非 0。
这通过可视为文本着色的机制实现:参数 "arg" 的值有附加属性(如文本颜色),使第一个 "db a" 与第二个不同。
若在另一宏内定义宏,内层宏文本会保留着色。
下例用此机制改进早前 "state"/"switch" 宏:

    macro setup variable

            variable = 0

            macro state
                    db variable
            end macro

            macro switch value
                    variable = variable xor (value)
            end macro

    end macro

    setup GLOBAL_STATE

    namespace Program

            switch 8

            GLOBAL_STATE = 0

            state

    end namespace

汇编 "setup GLOBAL_STATE" 时,"switch" 宏体含文本:
GLOBAL_STATE = GLOBAL_STATE xor (value)
其中两处 "GLOBAL_STATE" 均附着全局上下文。汇编 "switch 8" 时宏生成行:
GLOBAL_STATE = GLOBAL_STATE xor (8)
此时 "8" 带着调用 "switch" 处的上下文。
此变体 "switch" 与 "state" 无论何处调用都使用全局变量,类似许多语言中的闭包。
变量仍全局,其他代码仍可干扰。要完全隔离,可用 "local" 创建特殊参数:
同名文本但着色为宏实例唯一上下文:

    macro setup

            local variable

            variable = 0

            macro state
                    db variable
            end macro

            macro switch value
                    variable = variable xor (value)
            end macro

    end macro

    setup

此类上下文传递也发生在符号变量的值上。用 "equ" 或 "define" 定义时,整个值标记为定义时的上下文。
用 "match" 或 "irpv" 将此类文本赋给参数时,各片段携带的上下文信息保留:

    First:
            .x = 1

    define LIST .x

    Second:
            .x = 2

    LIST equ LIST, .x

    match values, LIST
            display `values
            db values
    end match

"display" 显示从 "LIST" 用 "match" 提取的文本为 ".x, .x",但两标识符文本上下文不同,列表含两个不同值。
当 "match" 将标识符文本切成多段,各段保留原文本的上下文。
若标识符由不同来源的文本片段拼成,仅初始部分的上下文影响符号识别。
可在标识符前加 "#" 强制在当前上下文识别:

    macro tester name
            namespace my
                    name db ?       ; symbol defined in its original namespace
                    #name db ?      ; symbol defined in "my" namespace
            end namespace
    end macro 

若仍不够,需剥离文本上一切上下文,"rawmatch" 伪指令正是如此。
类似效果可用 ` 将参数值转为字符串再用 "eval" 解释。
宏参数前加 "&" 不向参数值添加上下文,但也不移除已有上下文。可安全传递文本而不丢失信息,
也不污染 unwanted 上下文。多阶段示例:

    calminstruction (var) transparent_equ &val&
            publish :var, val
    end calminstruction

    namespace Windows
            EOL := string 0x0A0D
            link equ EOL
    end namespace

    EOL := 0x0A

    match WinEOL, Windows.link
            list    equ             WinEOL, EOL ; added context
            list    transparent_equ WinEOL, EOL     ; no context added
    end match

    namespace C64
            EOL := 0x0D
            irpv items, list
                    db items
            end irpv
    end namespace

传给 "db" 的项文本均为 "EOL",但解释不同:Windows 命名空间上下文、全局上下文、
最后为无上下文原始文本(此时指向最近定义,即 C64 命名空间中的 EOL)。

如何定义与核心伪指令同名的指令?

语言大体可用宏实现,但需含与汇编器伪指令同名的命令。虽可用宏覆盖任何指令,
宏自身可能需要访问原伪指令。要使同名在不同上下文调用不同指令,可实现语言所在命名空间含覆盖宏,
需原伪指令的宏须临时切换到未覆盖的命名空间——每个此类宏须包在 "namespace" 块中。
还有一技巧,与宏参数或符号变量文本保留符号应被解释的上下文有关(含基命名空间与以点开头的父标号)。
宏文本通常不携带此类信息,但若宏构造方式使其含曾作为另一宏参数或符号变量一部分的文本,
则成为新宏一部分时仍保留上下文信息。例如:

    macro definitions end?
            namespace embedded
            struc LABEL? size
                    match , size
                            .:
                    else
                            label . : size
                    end match
            end struc
            macro E#ND? name
                    end namespace
                    match any, name
                            ENTRYPOINT := name
                    end match
                    macro ?! line&
                    end macro
            end macro
    end macro

    definitions end

    start LABEL
    END start

给 "definitions" 宏的参数看似无用(每个 "end" 仍替换为 "end"),但来自参数的文本带上下文属性,
成为新宏一部分时保留。因此 "LABEL" 宏可在 "end" 指令已具不同含义的命名空间中使用,
宏体内的 "end" 仍指外层命名空间的符号。
本例参数大小写不敏感,会替换 "macro" 语句中本应定义在 "embedded" 命名空间的 "END"。
故用连接运算符拆开标识符以免被识别为参数。参数大小写敏感时则不必。
用符号变量代替宏参数,经 "match" 提取符号变量文本,可达同样效果:

    define link end
    match end, link
            namespace embedded
            struc LABEL? size
                    match , size
                            .:
                    else
                            label . : size
                    end match
            end struc
            macro END? name
                    end namespace
                    match any, name
                            ENTRYPOINT := name
                    end match
                    macro ?! line&
                    end macro
            end macro
    end match

    start LABEL
    END start

不经符号变量传递文本则不行,因 "match" 等控制伪指令定义的参数不会向文本添加上下文(除非已有)。
CALM 指令提供另一类解法。若定制指令集全以 CALM 定义,可能根本不需要原控制伪指令。
但若 CALM 需汇编可能不可达的伪指令,传给 "assemble" 的符号变量应为指令符号定义适当上下文。

如何将宏指令转换为 CALM?

经典宏指令由文本行组成,每次调用时预处理(用参数值替换参数名),再将预处理行送汇编。
例如下宏只生成一行待汇编行,将 "number" 替换为唯一参数给出的文本:

    macro octet value*
            db value
    end macro

CALM 指令可视为定制预处理器,须用特殊语言编写。可用各种命令处理参数并生成待汇编行。
基本层面也可用 "arrange" 模拟标准预处理,之后须用 "assemble" 显式送汇编:

    calminstruction octet value*
            arrange value, =db value
            assemble value
    end calminstruction

结果与原宏相同,因进行同类预处理。但与宏文本不同,给 "arrange" 的模式须显式说明哪些名 token 替换为值、
哪些(以 "=" 为前缀)保持不动。从模式复制的 token 剥离一切上下文,如宏文本通常不携带
(而来自实参的值保留指令启动时的识别上下文)。
这是最直接转换法;"arrange" 与 "assemble" 序列可生成与原宏相同的行。但有一例外——宏执行 "local" 时,
创建指向该指令实例唯一命名空间中符号的预处理参数。

    macro pointer
            local next
            dd next
        next:
    end macro

CALM 无此类命名空间;CALM 的局部命名空间在所有实例间共享。故每次调用需唯一符号时须手动构造,
例如向名称追加唯一数:

    global_uid = 0

    calminstruction pointer
            compute global_uid, global_uid + 1
            local command
            arrange command, =dd =next#global_uid
            assemble command
            arrange command, =next#global_uid:
            assemble command
    end calminstruction

此处 "arrange" 的变量为数值,须替换为文本。仅当值为无非负项的纯非负数时可行,
此时 "arrange" 转为含该数十进制表示的文本 token。送汇编的行将含 "next#1" 等标识符。
全局计数器也可用 "arrange" 准备 "global_uid = global_uid + 1" 再送汇编递增;
"compute" 可在 CALM 处理器内直接完成。且不受改变汇编上下文的因素影响。
若指令为无条件且在跳过的 IF 块内使用,"compute" 仍会执行,因 CALM 命令执行——如标准预处理——
独立于主汇编流。对 "global_uid" 的引用始终指向 CALM 指令定义并编译时作用域中的同一符号。
用 "compute" 递增更可靠可预测。
类似地,定义标号的那行汇编可用 "publish" 代替。标号值(应等于含 "dd" 的行汇编后地址)须先计算,
因 "publish" 只执行符号赋值:

    global_uid = 0

    calminstruction pointer
            compute global_uid, global_uid + 1
            local symbol, command
            arrange symbol, =next#global_uid
            arrange command, =dd symbol
            assemble command
            local address
            compute address, $
            publish symbol:, address
    end calminstruction 

因 CALM 指令本身有条件,其内 "publish" effectively 也有条件,故正确替代带标号行的汇编。
全局计数器有诸多优点但可能被干扰,有时局部计数器更好。但 CALM 局部命名空间通常外部不可访问,
给此类计数器初值较难。可用 "take" 检查计数器是否已初始化:

    calminstruction pointer
            local id
            take id, id
            jyes increment
            compute id, 0
        increment:
            compute id, id + 1
            local symbol, command
            arrange symbol, =next#id
            arrange command, =dd symbol
            assemble command
            local address
            compute address, $
            publish symbol:, address
    end calminstruction 

但这增加每次调用都执行的命令。更好解法利用定义 CALM 时可定义定制语句:

    calminstruction calminstruction?.init? var*, val:0
            compute val, val
            publish var, val
    end calminstruction

    calminstruction pointer
            local id
            init id, 0
            compute id, id + 1
            local symbol, command
            arrange symbol, =next#id
            arrange command, =dd symbol
            assemble command
            local address
            compute address, $
            publish symbol:, address
    end calminstruction 

定制语句 "init" 在定义 CALM 指令时调用(不向被定义指令生成可执行命令——若需生成须自行 "assemble")。
它接收 CALM 指令局部作用域中的变量名,用 "publish" 赋初始数值。
用符号值初始化局部变量,更简单的定制指令即可:

    calminstruction calminstruction?.initsym? var*, val&
            publish var, val
    end calminstruction

"val" 参数文本携带含 "initsym" 语句的 CALM 定义处的识别上下文,故可为 "assemble" 准备含局部符号引用的文本:

    calminstruction bigendian32? value
            local command
            initsym command, dd value
            compute value, value bswap 4
            assemble command
    end calminstruction 

编译后此指令仅含 "compute" 与 "assemble" 两条实际命令;局部符号 "command" 的值为文本,
在相同局部上下文解释,与 "compute" 引用同一 "value" 符号。
此例还演示 CALM 相对标准宏的另一优点:严格语义防止简单文本替换允许的多种不良行为。
"value" 文本由 "compute" 作为数值子表达式求值,意外语法会报错。故宜全程用 CALM 命令处理参数,
仅最终简单语句用 "assemble"。存在同等 CALM 命令时甚至可省去 "assemble":

    calminstruction bigendian32? value
            emit 4, value bswap 4
    end calminstruction 

操作完全独立于标准汇编过程;若含指令为无条件,即使在跳过的 "if" 块内或定义宏时调用也会生成输出
(且不向宏定义添加任何行)。


扫描二维码,在手机上阅读!

评论已关闭

© 2026 小工具搬运工. . 京ICP备17053127号