assembly - 我的 memset 实现结果仅打印更改,而不是整个结果字符串

标签 assembly x86-64 att memset

这与 memset movq giving segfault 中的实现实验相同。 我一直在打印 memset 的结果,它似乎只打印出更改,而不打印字符串的其余部分。

experimentMemset:   #memset(void *ptr, int value, size_t num)

    movq %rdi, %rax     #sets rax to the first pointer, to return later

.loop:
        cmp $0, %edx    #see if num has reached limit
        je .end                

        movq %rsi, (%rdi)       #copies value into rdi
        inc %rdi                #increments pointer to traverse string
        subl $1, %edx   #decrements count
        jmp .loop
.end:
        ret



int main {

 char str[] = "almost every programmer should know memset!";
    printf("MEMSET\n");
    my_memset(str, '-', 6);
    printf("%s\n", str);

}

我的输出:------

cplusplus.com 的正确输出:------ 每个程序员都应该知道 memset!

最佳答案

movq 将高位零存储在 int value 中,而不仅仅是低字节。这会终止 C 字符串。并且还会写入调用者传递的 ptr+length 的末尾!

使用mov %sil, (%rdi)存储1个字节。

(事实上,您使用 movq 存储 8 个字节,包括根据调用约定允许包含垃圾的高 4 个字节,因为它们不是 32 位 int value。不过,对于这个调用者,它们也将为零。)

您可以通过使用调试器或更好的测试工具检查内存内容来检测到这一点。下次再这样做。更好的调试调用者可以使用 write 或 fwrite 来打印完整的缓冲区,并且您可以将其通过管道传输到 hexdump -C 中。或者只是使用 GDB 的 x 命令来转储内存字节。


您只检查%edx,即%rdxsize_t num的低4个字节。如果调用者要求您准确设置 4GiB 内存,您将返回而不存储任何内容。


您可以通过将条件分支放在底部来使循环更加紧凑。您可以将声明更改为unsigned num,或者修复您的代码。

.globl experimentMemset
experimentMemset:   #memset(void *ptr, int value, size_t num)

    movq %rdi, %rax     #sets rax to the first pointer, to return later

    test  %rdx, %rdx    # special case: size = 0, loop runs zero times
    jz    .Lend
.Lloop:                   # do{
      mov   %sil, (%rdi)     # store the low byte of int value
      inc   %rdi             # ++ptr
      dec   %rdx
      jnz  .Lloop         # }while(--count);
.Lend:
    ret

甚至不再有任何指令:我只是将 cmp/jcc 从循环中拉出,使其成为跳过循环检查,并将底部的 jmp 转换为 jcc 读取由 dec 设置的标志。


效率

当然,一次存储 1 个字节的效率非常低,即使我们优化循环以便更多的 CPU 可以在每个时钟迭代 1 次时运行它。对于高速缓存中热的中型阵列,现代 CPU 使用 AVX 或 AVX512 存储可以将速度提高 32 到 64 倍。在具有 ERMSB 功能的 CPU 上,使用 rep stosb 字符串指令可以接近对齐缓冲区的速度。是的,x86 有一条实现 memset 的指令!

(或者对于更广泛的模式,wmemsetrep stosd。在没有 ERMSB 但具有快速字符串的 CPU 上(PPro 以及 IvyBridge 之前的更高版本),rep stosd 或 stosq 更快,因此您可以 imul $0x01010101, %esi, %eax 来广播低字节。)

# slowish for small or misaligned buffers
# but probably still better than a byte loop for buffers larger than maybe 16 bytes
.globl memset_ermsb
memset_ermsb:   #memset(void *ptr, int value, size_t num)

    mov  %rdx, %rcx            # count = num
    mov  %esi, %eax            # AL = char to set
    rep  stosb                 # destination = RDI 
    ret

真正的 memset 实现使用 SIMD 循环,因为这对于较小或未对齐的缓冲区来说速度更快。关于优化 memset/memcpy 的文章已经很多了。 Glibc 的实现非常聪明,是一个很好的例子。

内核代码无法轻松使用 FPU/SIMD,因此 rep stos memset 和 rep movsb memcpy 确实在 Linux 内核的现实生活中使用。

关于assembly - 我的 memset 实现结果仅打印更改,而不是整个结果字符串,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60502624/

相关文章:

c - 理解反汇编——看两个 main() 的

c - while(i--) 通过 gcc 和 clang 优化 : why don't they use sub/jnc?

c - ASM - 优化的 `for` 循环

C - 在 Mac OSX Lion 上编译时架构 x86_64 的 undefined symbol

c - 如何通过内存分配特性来衡量int和short变量的大小?

assembly - 如何确定常量字符串的长度?

assembly - 如何编译汇编代码?

c - 使用金丝雀值的堆栈损坏检测

assembly - cmp汇编语言指令-gas格式

assembly - 如何将此 ATT 程序集转换为英特尔语法?不使用寄存器跳转到非相对地址