【问题标题】:string comparison in 80868086中的字符串比较
【发布时间】:2015-11-08 09:47:47
【问题描述】:

我对这个问题有疑问。我不知道它想从我这里得到什么。

问题:编写一个程序,将 DS:SI 处的源字符串与 ES:DI 处的目标字符串进行比较,并相应地设置标志。如果源小于目标,则设置进位标志。如果字符串相等,则设置零标志。如果源大于目标,则清零和进位标志。

我的回答:

MOV ESI , STRING1
MOV EDI, STRING2
MOV ECX, COUNT
CLD
REPE CMPSB

我仍然不确定。是真的还是我应该尝试其他方法?

ps:我不明白为什么人们会否决这个问题。我的问题有什么问题?我想我们都是来学习的。或不 ?有什么想念的吗?

【问题讨论】:

  • 我也不知道,但你难道没有办法随时尝试运行它吗?如果没有,那么这种方法值得采用——前提是您对该主题有更多问题。
  • 那么,在运行上面的代码之后,不同字符串的零和进位标志如何?他们是对的吗?然后你就完成了。他们错了,但仍然拥有所有 3 种不同结果的信息吗?然后,您只需要添加更多代码来修复标志以匹配要求。您是否没有足够的信息,因此无法检测到所有 3 种不同的结果?然后您需要完全更改代码或添加更多代码以区分所有 3 种可能性,然后 然后 使标志与要求匹配。
  • 8086 是 16 位 .... 所以你没有 32 位寄存器。而不是cld,我会选择cmp al,al来清除进位并设置零来处理COUNT=0时的情况
  • 哦,还有“写一个过程”,这可能意味着你必须写一个可调用的子程序,它以标志的形式返回结果?如果是这种情况(您应该根据您的课程材料知道),您应该在问题中包含您的完整程序,包括输入/​​标签和返回......(当然,如果您将答案公开发布在互联网,以及您课程中的其他人复制它...... SO的家庭作业问题可能会像这样有问题。)
  • @hyde +1 并且只需要添加可能有也可能没有一些调用约定从堆栈中获取操作数...

标签: string assembly x86 x86-16 strcmp


【解决方案1】:

如果问题陈述表明当你被调用时指针已经在 SIDI 中,你不应该破坏它们。

16 位代码通常不遵守所有函数的单一调用约定,并且在寄存器中传递(前几个)args 通常是好的(更少的指令,并避免存储/重新加载)。 32 位 x86 调用约定通常使用堆栈参数,但这已经过时了。 Windows x64 和 Linux/Mac x86-64 System V ABI / 调用约定都使用寄存器参数。


不过,问题陈述并未提及计数。 因此,您正在为以零字节终止的字符串实现strcmp,而不是为已知长度的内存块实现memcmp您不能使用单个rep 指令,因为您需要检查不相等和字符串结尾。 如果您只是传递一些较大的尺寸,并且字符串 相等,repe cmpsb 将继续通过终止符。

如果您知道 either 字符串的长度,则

repe cmpsb 可用。例如在 CX 中取一个长度 arg 以避免在两个字符串中都超过终止符的问题。

但就性能而言,repe cmpsb 无论如何都不是很快(例如,在 Skylake 与 Ryzen 上,每次比较需要 2 到 3 个周期。甚至在 Bulldozer 系列上每次比较需要 4 个周期)。只有 rep movsrep stos 在现代 CPU 上是高效的,具有一次复制或存储 16(或 32 或 64)字节的优化微码。

在内存中存储字符串有 2 个主要约定:显式长度字符串(指针 + 长度),如 C++ std::string,以及 隐式长度字符串有一个指针,字符串的结尾由哨兵/终止符标记。 (例如使用 0 字节的 C char*,或使用 '$' 作为终止符的 DOS 字符串打印函数。)


一个有用的观察是您只需要检查 一个 字符串中的终止符。如果另一个字符串有终止符而这个没有,那将是不匹配的。

因此,您希望将一个字节从一个字符串加载到寄存器中,并检查它的终止符和另一个字符串的内存。

(如果您需要实际使用 ES:DI 而不是仅使用默认 DS 段库的 DI,则可以使用 cmp al, [es: bx + di](NASM 语法,根据需要进行调整,例如 cmp al, es: [bx + di] I想想)。可能是你打算使用lodsbscasb的问题,因为scasb uses ES:DI。)

;; inputs:  str1 pointer in DI, str2 pointer in SI
;; outputs: BX = mismatch index, or one-past-the-terminators.
;;          FLAGS:  ZF=1 for equal strings (je),  ZF=0 for mismatch (jne)
;; clobbers: AL (holds str1's terminator or mismatching byte on return)
strcmp:
    xor    bx, bx
.innerloop:                 ; do { 
    mov    al, [si + bx]        ; load a source byte
    cmp    al, [di + bx]        ; check it against the other string
    jne   .mismatch             ; if (str1[i] != str2[i]) break;

    inc    bx                   ; index++

    test   al, al               ; check for 0.  Use cmp al, '$' for a $ terminator
    jnz   .innerloop        ; }while(str1[i] != terminator);

    ; fall through (ZF=1) means we found the terminator
    ; in str1 *and* str2 at the same position, thus they match

.mismatch:               ; we jump here with ZF=0 on mismatch
    ; sete  al           ; optionally create an integer in AL from FLAGS
    ret

用法:将指针放入SI/DI,call strcmp/je match,因为匹配/不匹配状态为FLAGS。如果要将条件转为整数,386及以后的CPU允许sete al根据equals条件(ZF==1)在AL中创建0或1。

使用sub al, [mem] 而不是cmp al, [mem],我们会得到al = str1[i] - str2[i],只有当字符串匹配时才给我们一个0。如果您的字符串仅包含 0..127 的 ASCII 值,则不会导致有符号溢出,因此您可以将其用作有符号返回值,该值实际上告诉您哪个字符串在另一个之前/之后排序。 (但如果字符串中可能有高 ASCII 128..255 字节,我们需要零或符号扩展为 16 位 first 以避免像 ( unsigned)5 - (unsigned)254 = (signed)+7,因为 8 位环绕。

当然,有了我们的FLAGS返回值,调用者已经可以使用jajb(无符号比较结果),或者jg/jl如果他们想处理字符串为持有signed char。无论输入字节的范围如何,这都有效。

或者内联这个循环,以便jne mismatch 直接跳转到有用的地方

16-bit addressing modes are limited,但 BX 可以是基数,SI 和 DI 都可以是索引。我使用了索引增量而不是inc siinc di。使用lodsb 也是一种选择,甚至可能使用scasb 将其与其他字符串进行比较。 (然后检查终结符。)


性能

在某些现代 x86 CPU 上,索引寻址模式可能会更慢,但这确实可以节省循环中的指令(因此它适用于代码大小很重要的真正 8086)。虽然要真正调整 8086,但我认为 lodsb / scasb 将是您最好的选择,替换 mov 负载和 cmp al, [mem],以及 inc bx。如果您的调用约定不能保证这一点,请记住在循环外使用cld

如果您关心现代 x86,请使用 movzx eax, byte [si+bx] 打破对 EAX 旧值的错误依赖,用于不单独重命名部分寄存器的 CPU。 (如果你使用sub al, [str2],打破错误的 dep 尤其重要,因为这会将它变成一个通过 EAX 的 2 周期循环承载的依赖链,在除 PPro 之外的 CPU 上通过 Sandybridge。IvyBridge 和更高版本不会单独重命名 AL EAX,所以mov al, [mem] 是一个微融合加载+合并uop。)

如果cmp al,[bx+di] 微熔断负载,并用jne 宏熔断到一个比较和分支微指令中,则整个循环在 Haswell 上总共可能只有 4 微指令,并且每个循环可以运行 1 次迭代大输入时钟。最后的分支错误预测将使小输入性能更差,除非每次分支都以足够小的输入进行。见https://agner.org/optimize/。最近的 Intel 和 AMD 可以每个时钟执行 2 次加载。

展开可以摊销inc bx 的成本,但仅此而已。在循环内有一个采用 + 未采用的分支,当前的 CPU 每次迭代的运行速度都不能超过 1 个周期。 (有关 do{}while 循环结构的更多信息,请参阅 Why are loops always compiled into "do...while" style (tail jump)?)。为了更快,我们需要一次检查多个字节。

与使用 SSE2 的每 1 或 2 个周期 16 个字节相比,即使是 1 个字节/周期也非常慢(使用一些巧妙的技巧来避免读取可能出现故障的内存)。

有关使用 x86 SIMD 进行字符串比较以及 glibc 的 SSE2 和后来优化的字符串函数的更多信息,请参阅 https://www.strchr.com/strcmp_and_strlen_using_sse_4.2


GNU libc's 后备标量 strcmp 实现看起来不错(从 AT&T 转换为 Intel 语法,但保留了 C 预处理器宏和其他内容。L() 制作了一个本地标签)。

它仅在 SSE2 或更高版本不可用时使用。有用于检查整个 32 位寄存器的任何零字节的 bithacks,即使没有 SIMD,这也可以让你走得更快,但对齐是一个问题。 (如果终止符可以在任何地方,则在一次加载多个字节时必须小心,不要从您不确定的任何内存页面中读取确定至少包含 1 个字节的有效数据,否则您可能会出错。)

strcmp:
         mov    ecx,DWORD PTR [esp+0x4]
         mov    edx,DWORD PTR [esp+0x8]     # load pointer args

L(oop): mov     al,BYTE PTR [ecx]          # movzx eax, byte ptr [ecx] would be avoid a false dep
        cmp     al,BYTE PTR [edx]
        jne     L(neq)
        inc     ecx
        inc     edx
        test    al, al
        jnz     L(oop)

        xorl    eax, eax
        /* when strings are equal, pointers rest one beyond
           the end of the NUL terminators.  */
        ret

L(neq): mov    eax,  1
        mov    ecx, -1
        cmovb  eax, ecx
        ret

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-21
    • 1970-01-01
    • 1970-01-01
    • 2020-08-23
    • 2011-07-06
    相关资源
    最近更新 更多