PAYATHON 2026

Motorola 68000:栈传参过滤字符串并返回长度

技术老齐

结论

任务理解无误:按照原顺序遍历 A。如果字符在 B 中出现过,就跳过,否则写入 C。A 中的重复字符不会被去重,只要该字符不在 B 中,每次出现都会保留。

原始的嵌套扫描版本逻辑正确。位图版本在给定条件下也可以正常工作,栈偏移和位索引的计算都没有问题。它把最坏情况下的复杂度从 O(|A| × |B|) 降到了 O(|A| + |B|),需要额外使用 32 字节临时空间。

代码还有几处可以调整:

  • 将部分 CLR.W Dn 改为 MOVEQ #0,Dn,以免寄存器的高 16 位残留旧值。
  • 使用 LINK/UNLK 建立栈帧,这样局部空间发生变化时,参数偏移仍保持不变。
  • 明确要求 C 的容量至少为 strlen(A) + 1,否则可能发生越界。
  • 只有调用约定允许子程序破坏相关寄存器时,才可以不保存寄存器。
  • CMPM.B 每次仍只比较一个字节,不属于并行比较。

示例字符串的结果应为:

C = "He"
length = 2

原始版本是否正确

原始版本实现的是稳定过滤:

C = [A 中所有不属于字符集合 B 的字节]

这不是数学意义上的集合差,因为 A 中的重复字符不会被删除。例如:

A = "banana"
B = "bn"
C = "aaa"

调用前依次压入 &C、&B、&A,随后 BSR 压入返回地址。子程序入口处的栈布局如下:

0(A7)   return address
4(A7)   &A
8(A7)   &B
12(A7)  &C

因此,原始代码中的参数偏移是正确的。

MOVE.B 本身会更新条件码,可以删掉后面的 TST.B:

MOVE.B  (A0)+,D1
BEQ.S   Finish

扫描 B 时同样可以这样写:

MOVE.B  (A3)+,D2
BEQ.S   NotFound

这是精简原始版本最直接的方式。

位图版本的正确性

256 个可能的字节值需要 256 bit,即 32 bytes。对于字符值 ch:

byte offset = ch >> 3
bit number  = ch & 7

代码只显式计算了 ch >> 3。这样写没有问题,因为 68000 对内存操作数执行动态 BTST、BSET 时,只会使用位编号的低 3 位,相当于自动执行 ch & 7:

MOVE.W  D1,D2
LSR.W   #3,D2
BSET    D1,(A7,D2.W)

分配 32 字节位图后,栈布局变为:

0(A7)..31(A7)  bitmap
32(A7)         return address
36(A7)         &A
40(A7)         &B
44(A7)         &C

所以编辑版中的 36(A7)、40(A7) 和 44(A7) 也是正确的。

直接使用不断变化的 A7 作为局部数据基址,维护起来比较麻烦。以后只要增加一次寄存器保存操作,所有参数偏移都要重新计算。使用 A6 建立栈帧会更清楚。

建议的位图实现

下面的代码保留原有调用方式,并使用 A6 固定参数位置。该版本返回 16 位长度,因此 C 的最大有效长度不能超过 65535。

; Caller:
;   PEA.L   StringC
;   PEA.L   StringB
;   PEA.L   StringA
;   BSR     FilterLen_Bitmap
;   ADDA.L  #12,A7
;
; Entry stack layout:
;   4(A7)  = &A
;   8(A7)  = &B
;  12(A7)  = &C
;
; OUT:
;   D0.W = length of C
;
; MOD:
;   D0-D2, A0-A2
;
; PRECONDITIONS:
;   A and B are zero-terminated.
;   C has room for strlen(A) + 1 bytes.

FilterLen_Bitmap:
        LINK    A6,#-32             ; 32-byte local bitmap

; After LINK:
;    8(A6) = &A
;   12(A6) = &B
;   16(A6) = &C
;  -32(A6)..-1(A6) = bitmap

; Clear bitmap
        LEA     -32(A6),A2
        MOVEQ   #7,D2

ClearBitmap:
        CLR.L   (A2)+
        DBF     D2,ClearBitmap

; Build bitmap from B
        LEA     -32(A6),A2
        MOVEA.L 12(A6),A0
        MOVEQ   #0,D1

BuildBitmap:
        MOVE.B  (A0)+,D1
        BEQ.S   StartFilter

        MOVE.W  D1,D2
        LSR.W   #3,D2
        BSET    D1,0(A2,D2.W)
        BRA.S   BuildBitmap

; Filter A into C
StartFilter:
        MOVEA.L 8(A6),A0
        MOVEA.L 16(A6),A1
        MOVEQ   #0,D0              ; result length
        MOVEQ   #0,D1

NextA:
        MOVE.B  (A0)+,D1
        BEQ.S   FilterDone

        MOVE.W  D1,D2
        LSR.W   #3,D2
        BTST    D1,0(A2,D2.W)
        BNE.S   NextA              ; character occurs in B

        MOVE.B  D1,(A1)+
        ADDQ.W  #1,D0
        BRA.S   NextA

FilterDone:
        CLR.B   (A1)
        UNLK    A6
        RTS

MOVEQ #0,D0 会清除整个寄存器。原代码中的 CLR.W D0 只清除低 16 位。调用者只读取 D0.W 时,这不会影响结果;如果把 D0 当作 32 位返回值使用,其高 16 位可能仍是旧数据。

编辑版中的 CLR.W D1 同样不会清除高 16 位。当前代码只使用 D1 的低字节作为字符值,并采用 .W 索引,所以暂时不会出错,但完整清零更稳妥。

是否需要容量检查

接口没有传入 C 的容量时,子程序无法判断下一次写入是否会越界。可以把“caller guarantees size”作为底层接口约定,但应明确写出:

capacity(C) >= strlen(A) + 1

其中的 +1 用于存放字符串终止符。

如果输入来自不可信来源,可以将接口改为:

FilterLen_Bitmap(&A, &B, &C, capacity)

每次写入字符前检查:

length < capacity - 1

还应提前约定空间不足时的处理方式,例如:

  • 返回负数表示空间不足;
  • 通过状态码报告错误,并通过另一个输出参数返回实际长度;
  • 截断 C,同时保证末尾写入 0。

当 capacity == 0 时,程序连终止符都不能写,需要单独处理。

CMPM.B 是否合适

CMPM.B 不适合这里,“字节并行比较”这一说法也不准确。CMPM.B (Ax)+,(Ay)+ 每次只比较一个字节,然后分别递增两个地址寄存器。

当前算法需要固定 A 中的一个字符,同时不断递增 B 的扫描指针。普通的 CMP.B 更符合这种访问模式:

CMP.B   D2,D1
BEQ.S   NextCharA

CMPM.B 更适合两个连续序列同步前进的比较,例如逐字节判断两个字符串是否相同。

Word/long 操作能否加速过滤

只保证对齐还不够。过滤时必须逐个判断字节是否属于 B,输出位置又取决于前面保留了多少字符,因此不能简单改成:

MOVE.L
CMP.L

来一次处理 4 个字符。

MOVE.L 适合用来清空 32 字节位图,因为这部分确实属于连续且无条件的内存操作。如果想一次处理 A 中的多个字节,还要加入零字节检测、逐字节位图查询和压缩写入逻辑。在普通 68000 上,这种实现复杂得多,处理短字符串时也未必更快。

是否拆分 CharInString

在嵌套扫描版本中,将 B 的扫描过程提取为子程序,可以让结构更清楚,但每处理一个 A 字符就要调用一次,还可能反复压栈和恢复寄存器。字符串较短,或者作业强调子程序设计时,这种写法可以接受。追求速度时,更适合把内部循环留在过滤子程序中。

位图版本已经将 membership test 缩减为一次 BTST,继续拆分成子程序的收益不大。

还需明确的调用约定

代码会修改 D0-D2 和 A0-A2。是否需要保存这些寄存器取决于程序采用的 calling convention,而不是处理器本身。

如果调用约定要求子程序保存其中某些寄存器,应在建立栈帧后保存,并在返回前恢复,例如:

MOVEM.L D1-D2/A0-A2,-(A7)
...
MOVEM.L (A7)+,D1-D2/A0-A2

D0 用于存放返回值,通常不能在返回前恢复。哪些寄存器属于 caller-saved,哪些属于 callee-saved,应在接口注释中注明。

另外,只有目标位于短分支范围内时才能使用 BSR.S。如果无法确定距离,写成 BSR FilterLen_Bitmap,由汇编器选择合适的编码会更稳妥。

备注:内容仅供参考。