如何在程序集中遍历一个字符串直到到达 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 增加指针(如 cmp
;lea 1(%rax), %rax
;jne
)可能会更糟,因为它会破坏 [=87= 的宏融合] 成一个单一的 uop。 (实际上,cmp $imm, (%reg)
/ jcc 的宏融合不会发生在像 Skylake 这样的英特尔 CPU 上。不过,cmp
微融合内存操作数。也许 AMD 融合了 cmp/jcc。)此外,您将使用比您想要的更高的 RAX 1 离开循环。
因此(在 Intel Sandybridge 系列上)movzx
(又名 movzbl
)加载字节并将其零扩展到 %ecx
和 test %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
。在单步执行代码时使用调试器检查寄存器值。
现在我只是弄清楚如何遍历一个字符串。如果代码没有意义,那是因为我对某些信息的解释有误。最糟糕的是,我真的不知道自己在做什么。
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 增加指针(如 cmp
;lea 1(%rax), %rax
;jne
)可能会更糟,因为它会破坏 [=87= 的宏融合] 成一个单一的 uop。 (实际上,cmp $imm, (%reg)
/ jcc 的宏融合不会发生在像 Skylake 这样的英特尔 CPU 上。不过,cmp
微融合内存操作数。也许 AMD 融合了 cmp/jcc。)此外,您将使用比您想要的更高的 RAX 1 离开循环。
因此(在 Intel Sandybridge 系列上)movzx
(又名 movzbl
)加载字节并将其零扩展到 %ecx
和 test %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
。在单步执行代码时使用调试器检查寄存器值。