如何遍历汇编中的字符串,直到达到null为止? (strlen循环)

问题描述 投票:3回答:1

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

strlen:

pushq %rbx
movq %rsi, %rbx


loop:
    cmp $0x00, (%rdi, %rbx)
    je end
    inc %rbx
    jmp loop

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

PS:有一个原因使我的标题看起来像是一个老头,第二次在他的计算机上尝试搜索“如何去google.com”。Superrrrnoob在这里试图学习一些汇编语言。我正在尝试为自己实现strlen函数。

assembly x86-64 gas att strlen
1个回答
3
投票

只需inc %rbx就可以增加指针值。 (%rbx)使用其值作为存储器地址来取消引用该寄存器。在x86上,每个字节都有其自己的地址(此属性称为“可字节寻址的字节”),并且地址只是适合寄存器的整数。

ASCII字符串中的字符全为1字节宽,因此将指针加1将移至ASCII字符串中的下一个字符。 (在UTF-8的字符位于1..127代码点范围之外的一般情况下,这是不正确的,但是ASCII是UTF-8的子集。)


术语:ASCII码0被称为NUL(一个L),而不是NULL。在C中,NULL是指针概念。 C样式的隐式长度字符串可以描述为0终止或NUL终止,但是“ null终止”正在滥用该术语。


您应该选择一个不同的寄存器(称为调用寄存器),因此您无需在函数中推送/弹出该寄存器。您的代码不会make任何函数调用,因此不需要将归纳变量保留在保留调用的寄存器中。

我没有在其他SO Q&A中找到一个good简单示例。它们或者像我在注释中链接的那样,在循环内有2个分支(包括一个无条件的jmp),或者它们浪费了增加指针计数器的指令。在循环内使用索引寻址模式并不可怕,但是在某些CPU上效率较低,因此我仍然建议进行指针递增->在循环后减去end-start。

这就是我写一个最小的strlen的方法,它一次只检查1个字节(缓慢而简单)。我将循环本身保持很小,这是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    $0, (%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的返回值是0字节的数组索引=数据长度[[not包括终止符。

如果您手动内联此代码(因为它只是一个3指令循环),则通常只需要指向0终止符的指针,这样就不会打扰子废话,只需在代码末尾使用RAX循环。

可以通过剥离第一个迭代或使用jmp以cmp / jne进入循环的方式来避免在第一次加载之前偏移LEA / INC指令(在第一次cmp之前花费2个周期的等待时间),后公司Why are loops always compiled into "do...while" style (tail jump)?

在LEMP之间增加cmp / jcc之间的LEA指针(例如cmp; lea 1(%rax), %rax; jne)可能会更糟,因为它会破坏cmp / jcc的宏融合成单个uop。 (实际上,cmp $imm, (%reg) / jcc的宏融合在像Skylake这样的Intel CPU上都不会发生。cmp微融合内存操作数。也许AMD融合了cmp / jcc。)此外,您将离开RAX 1比您想要的高的循环。

因此,(在Intel Sandybridge系列上)加载movzx(又名movzbl)并将字节零扩展到%ecxtest %ecx, %ecx / jnz就像循环条件一样有效。但是更大的代码大小。


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

大字符串每次检查1个字节的速度比SSE2慢16倍。如果您不打算最小化代码大小和简化性,请参阅Why is this code 6.5x slower with optimizations enabled?以获取使用SSE2的简单strlen XMM寄存器。 SSE2是x86-64的基准,因此,在提速时,应始终使用它,因为值得在asm中手工编写的内容。


Re:您更新的问题

,其中包含Why does rax and rdi work the same in this situation?中实施的错误端口RDI和RBX都保存指针。将它们加在一起不会成为有效地址!在您尝试移植的代码中,RCX(索引)在循环之前被初始化为零。但是您没有执行xor %ebx, %ebx,而是执行了mov %rdi, %rbx。单步执行代码时,使用调试器检查寄存器值。
© www.soinside.com 2019 - 2024. All rights reserved.