如何在程序集中遍历一个字符串直到到达 null? (strlen 循环)

How to traverse a string in assembly until I reach null? (strlen loop)

现在我只是弄清楚如何遍历一个字符串。如果代码没有意义,那是因为我对某些信息的解释有误。最糟糕的是,我真的不知道自己在做什么。

strlen:

pushq %rbx
movq %rsi, %rbx


loop:
    cmp [=10=]x00, (%rdi, %rbx)
    je end
    inc %rbx
    jmp loop

end:
    movq %rbx, %rax
    popq %rbx
    ret

PS:我的标题看起来像一个老人第二次在他的计算机上试图搜索 "how to go to google.com" Superrrr noob 在这里试图学习一点汇编是有原因的。我正在尝试为自己实现 strlen 函数。

您只需 inc %rbx 增加指针值。 (%rbx) 取消引用该寄存器,使用它的值作为内存地址。在 x86 上,每个字节都有自己的地址(这个 属性 称为 "byte addressable"),地址只是适合寄存器的整数。

ASCII 字符串中的字符都是 1 字节宽,因此将指针递增 1 会移动到 ASCII 字符串中的下一个字符。 (在字符超出 1..127 代码点范围的 UTF-8 的一般情况下,情况并非如此,但 ASCII 是 UTF-8 的子集。)


术语:ASCII码0叫NUL(一L),不是NULL。在C语言中,NULL是一个指针概念。 C 风格的隐式长度字符串可以描述为以 0 结尾或以 NUL 结尾,但 "null-terminated" 误用了该术语。


你应该选择一个不同的寄存器(一个被调用破坏的寄存器)这样你就不需要 push/pop 它围绕你的函数。您的代码不会 进行 任何函数调用,因此无需将归纳变量保存在调用保留寄存器中。

我在其他 SO Q&A 中没有找到好的简单示例。他们要么在循环内有 2 个分支(包括一个无条件 jmp),就像我在评论中链接的那样,要么他们浪费指令递增一个指针 一个计数器。在循环内使用索引寻址模式并不可怕,但在某些 CPU 上效率较低,因此我仍然建议在循环后执行指针递增 -> 减去结束开始。

这就是我编写一次只检查 1 个字节的最小 strlen 的方式(缓慢而简单)。我保持循环本身很小,这是 IMO 一个合理的例子,说明一般情况下编写循环的好方法。通常保持代码紧凑可以更容易理解 asm 中的函数。 (给它取一个不同于 strlen 的名称,这样你就可以在不需要 gcc -fno-builtin-strlen 或其他任何东西的情况下对其进行测试。)

.globl simple_strlen
simple_strlen:
    lea     -1(%rdi), %rax     # p = start-1 to counteract the first inc
 .Lloop:                       # do {
    inc     %rax                  # ++p
    cmpb    [=10=], (%rax)
    jne     .Lloop             # }while(*p != 0);
                           # RAX points at the terminating 0 byte = one-past-end of the real data
    sub     %rdi, %rax     # return length = end - start
    ret

strlen的return值是0字节的数组索引=数据的长度包括终止符。

如果您手动内联它(因为它只是一个 3 指令循环),您通常只需要一个指向 0 终止符的指针,这样您就不会为子废话而烦恼,只需在循环结束。

在第一次加载之前避免偏移 LEA/INC 指令(在第一次 cmp 之前花费 2 个延迟周期)可以通过剥离第一次迭代来完成,或者使用 jmp 进入在 cmp/jne 处循环,在 inc. 之后。 .

在 cmp/jcc 之间使用 LEA 增加指针(如 cmplea 1(%rax), %raxjne)可能会更糟,因为它会破坏 [=87= 的宏融合] 成一个单一的 uop。 (实际上,cmp $imm, (%reg) / jcc 的宏融合不会发生在像 Skylake 这样的英特尔 CPU 上。不过,cmp 微融合内存操作数。也许 AMD 融合了 cmp/jcc。)此外,您将使用比您想要的更高的 RAX 1 离开循环。

因此(在 Intel Sandybridge 系列上)movzx(又名 movzbl)加载字节并将其零扩展到 %ecxtest %ecx, %ecx / jnz 作为循环条件。但是更大的代码量。


大多数 CPU 将 运行 我的循环在每个时钟周期迭代 1 次。通过一些循环展开,我们可能每个周期接近 2 个字节(同时仍然只分别检查每个字节)。

对于大型字符串,一次检查 1 个字节比我们使用 SSE2 慢 16 倍。 =32=] 用于使用 XMM 寄存器的简单 SSE2 strlen。 SSE2 是 x86-64 的基线,所以当它提供加速时你应该总是使用它,对于那些值得在 asm 中手写的东西。


回复:您更新的问题 带有来自

的实施的错误端口

RDI 和RBX 都持有指针。将它们加在一起并不能构成有效地址!在您尝试移植的代码中,RCX(索引)在循环之前被初始化为零。但是你没有 xor %ebx, %ebx,而是 mov %rdi, %rbx。在单步执行代码时使用调试器检查寄存器值。