x86-64 NASM Intel风格汇编中字符串反转时的数值篡改Bug排查问询
我写了一个无栈的x86-64 NASM Intel风格子程序,用来把整数转成字符串。汇编链接都没问题,但运行时遇到了奇怪的Bug:输入1691时,预期输出是"1691",实际却得到"1432"。我已经定位到问题出在_reverse_string里的mov [str_digits + rcx], bl这一行——如果去掉反转逻辑,输出是正确的逆序"1961",说明确实需要反转,但我搞不懂为什么当前逻辑会篡改数值。相关代码如下:
section .data section .bss str_digits resb 100 section .text global _start _start: mov rax, 1691 call _int_to_str call _exit _int_to_str: mov rbx, 10 mov rcx, 0 _conversion_loop: mov rdx, 0 div rbx add rdx, 48 mov [str_digits + rcx], dl inc rcx cmp rax, 0 jne _conversion_loop mov rbx, [str_digits] mov rdx, rcx _reverse_string: mov [str_digits + rcx], bl inc rbx dec rcx cmp rcx, 0 jne _reverse_string _output_string: mov rax, 1 mov rdi, 1 mov rsi, str_digits syscall ret _exit: mov rax, 60 mov rdi, 0 syscall
我做了这些调试工作:
- 确认
mov [str_digits + rcx], bl语法合法; - 用GDB调试,得到以下关键信息:
这里发现每次(gdb) disass _reverse_string => 0x000000000040103f <+0>: mov BYTE PTR [rcx+0x402000],bl 0x0000000000401045 <+6>: inc rbx 0x0000000000401048 <+9>: dec rcx 0x000000000040104b <+12>: cmp rcx,0x0 0x000000000040104f <+16>: jne 0x40103f <_reverse_string> (gdb) info reg rcx rcx 0x4 4 (gdb) info reg rbx rbx 0x31363931 825637169 (gdb) info reg bl bl 0x31 49 # 单步执行后 (gdb) info reg rcx rcx 0x3 3 (gdb) info reg rbx rbx 0x31363932 825637170 (gdb) info reg bl bl 0x32 50inc rbx后,bl的值都会加1,完全不是我想要的原字符串的下一个字符; - 用strace跟踪,发现系统调用本身没问题,只是输出内容错误:
execve("./test3", ["./test3"], 0x7ffc5651b270 /* 91 vars */) = 0 write(1, "1432", 41432) = 4 exit(0) = ? +++ exited with 0 +++ - 尝试在
mov [str_digits + rcx], bl前加mov bl, [rbx],结果触发段错误。
注:我不是要问“怎么反转字符串”或“怎么实现整数转字符串”(这些我知道有解决方案),而是专门想搞懂这个数值篡改Bug的原因。
Bug原因分析
问题的核心出在你初始化rbx的方式上:
mov rbx, [str_digits]
在x86-64汇编里,[str_digits]表示加载内存地址str_digits处的内容到rbx中,而不是把str_digits这个地址本身加载到rbx(这才是你需要的指针)。
具体来说,_conversion_loop结束后,str_digits里存储的是逆序的字符:'1'(0x31)、'9'(0x39)、'6'(0x36)、'1'(0x31)(因为1691除以10的余数依次是1、9、6、1)。由于x86是小端模式,mov rbx, [str_digits]会把这四个字节(加上后面四个0字节)拼成一个64位数值0x0000000031363931,也就是十进制的825637169。
接下来的inc rbx是对这个数值加1,而不是对指针移动。所以每次执行inc rbx,rbx的值就会加1,对应的bl(rbx的低8位)也会加1:
- 初始bl是0x31('1')
- 第一次inc后,bl变成0x32('2')
- 第二次inc后,bl变成0x33('3')
- 第三次inc后,bl变成0x34('4')
然后看你的反转循环逻辑:
_reverse_string: mov [str_digits + rcx], bl ; 把bl的值写到str_digits+rcx的位置 inc rbx ; 数值rbx加1 dec rcx ; 索引rcx减1 cmp rcx, 0 jne _reverse_string
第一次循环时rcx是4,你把'1'写到str_digits+4的位置;第二次rcx是3,把'2'写到str_digits+3;第三次rcx是2,把'3'写到str_digits+2;第四次rcx是1,把'4'写到str_digits+1。循环结束后,str_digits里的内容变成:str_digits[0] = '1'(原内容没被修改)、str_digits[1]='4'、str_digits[2]='3'、str_digits[3]='2',所以输出就是"1432",完全符合你看到的错误结果。
至于你尝试加mov bl, [rbx]触发段错误,是因为rbx里存储的是0x31363931这样的数值,这个地址不属于你的程序内存空间,访问它自然会触发段错误。
关键修正点
如果你要继续用指针方式反转,正确的初始化应该是把str_digits的地址加载到rbx,而不是加载内容:
mov rbx, str_digits ; 把字符串的起始地址(指针)加载到rbx,而不是内容
之后的inc rbx才是移动指针,指向字符串的下一个字符,这样你才能通过mov bl, [rbx]读取到原字符串的下一个字符。
内容的提问来源于stack exchange,提问作者maybe

