Skip to content

汇编语言 - 字符串处理

字符串处理是编程中常见的需求。

在汇编中处理字符串意味着直接操作内存中的字节序列,虽然更底层但也更灵活。


字符串的定义和存储

在 NASM 中,字符串本质上是字节序列,可以用 DB 伪指令定义:

实例

asm
; 字符串定义方式  

section .data  

    ; 方式1:标准字符串  

    str1 db 'Hello, runoob!', 0     ; C 风格:以 null 结尾  

    ; 方式2:带换行符  

    str2 db 'Line1', 0xA, 'Line2', 0xA  

    ; 方式3:反引号支持转义  

    str3 db `hello\nworld\n`, 0     ; 自动转换转义序列  

    ; 方式4:逐个字符定义  

    str4 db 'A', 'B', 'C', 'D', 0  

    ; 方式5:使用 dup 生成重复字符  

    border db 40 dup('-')           ; 40 个减号  

    ; 字符串长度计算(编译时)  

    str1_len equ $ - str1           ; 包含结尾 null

计算字符串长度

有两种方式获取字符串长度:编译时计算(EQU)和运行时计算(遍历查找 null):

实例

asm
; 文件路径:strlen_demo.asm  

; 两种计算字符串长度的方式  

section .data  

    ; 方式A:编译时计算(适用于常量字符串)  

    msg1 db 'Hello, RUNOOB!', 0xA  

    msg1_len equ $ - msg1          ; 编译时自动计算  

    ; 方式B:null 结尾的字符串(运行时计算)  

    msg2 db 'Find my length', 0    ; 以 0 结尾  

section .text  

    global _start  

_start:  

    ; 方式A:直接使用编译时计算的长度  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, msg1  

    mov edx, msg1_len              ; 直接使用常量  

    int 0x80  

    ; 方式B:运行时计算 null 结尾字符串的长度  

    mov esi, msg2                  ; esi 指向字符串起始  

    mov ecx, 0                     ; 计数器  

strlen_loop:  

    cmp byte [esi], 0              ; 当前字节是 null?  

    je  strlen_done                ; 是,结束  

    inc ecx                        ; 计数 +1  

    inc esi                        ; 指针 +1  

    jmp strlen_loop  

strlen_done:  

    ; ecx 现在存放字符串长度(不含 null)  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, msg2  

    mov edx, ecx                   ; 使用计算出的长度  

    ; 这里有个问题:ecx 被覆盖了,应该先保存  

    ; 正确做法:push ecx 再 pop 到 edx  

    mov eax, 1  

    mov ebx, 0  

    int 0x80

字符串复制

使用循环逐字节复制,或使用 x86 的字符串操作指令 MOVSB

实例

asm
; 文件路径:strcpy_demo.asm  

; 字符串复制两种实现方式  

section .data  

    src db 'runoob source string', 0  

    src_len equ $ - src  

section .bss  

    dest_manual resb 64             ; 手动复制目标  

    dest_fast resb 64               ; 快速复制目标  

section .text  

    global _start  

_start:  

    ; 方式A:手动逐字节复制  

    mov esi, src                    ; 源地址  

    mov edi, dest_manual            ; 目标地址  

    mov ecx, src_len                ; 字节数  

copy_loop:  

    mov al, [esi]                   ; 读取一字节  

    mov [edi], al                   ; 写入一字节  

    inc esi                         ; 源指针++  

    inc edi                         ; 目标指针++  

    loop copy_loop  

    ; 方式B:使用字符串操作指令(更快)  

    cld                             ; 清除方向标志(DF=0,正向复制)  

    mov esi, src                    ; 源地址(ESI)  

    mov edi, dest_fast              ; 目标地址(EDI)  

    mov ecx, src_len                ; 字节数(ECX)  

    rep movsb                       ; 重复执行 movsb ecx 次  

    ; rep movsb:while(ecx>0) { [edi]=[esi]; esi++; edi++; ecx--; }  

    ; 验证:输出两个复制结果  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, dest_manual  

    mov edx, src_len  

    int 0x80  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, dest_fast  

    mov edx, src_len  

    int 0x80  

    mov eax, 1  

    mov ebx, 0  

    int 0x80

REP MOVSB 是 x86 最经典的字符串复制方式。但在现代 CPU 上,手动复制循环配合循环展开可能比 REP MOVSB 更快,因为现代 CPU 对简单操作有更好的流水线优化。


字符串比较

使用 CMPSB 配合 REPE(重复直到不等)逐字节比较:

实例

asm
; 文件路径:strcmp_demo.asm  

; 比较两个字符串是否相等  

section .data  

    str_a db 'runoob', 0  

    str_b db 'runoob', 0  

    str_c db 'RUNOOB', 0  

    eq_msg db 'Strings are equal', 0xA  

    eq_len equ $ - eq_msg  

    ne_msg db 'Strings are NOT equal', 0xA  

    ne_len equ $ - ne_msg  

section .text  

    global _start  

_start:  

    cld                             ; 正向比较  

    mov esi, str_a                  ; 第一个字符串  

    mov edi, str_b                  ; 第二个字符串  

    mov ecx, 7                      ; 最多比较 7 字节(含 null)  

    repe cmpsb                      ; 重复比较直到不等或 ecx=0  

    ; repe: 如果 ZF=1(相等)且 ecx>0 则继续  

    ; cmpsb: 比较 [esi] 和 [edi],然后 esi++, edi++  

    je  strings_equal               ; 如果 ZF=1,所有字节都相等  

    ; 不等  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, ne_msg  

    mov edx, ne_len  

    int 0x80  

    jmp compare_next  

strings_equal:  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, eq_msg  

    mov edx, eq_len  

    int 0x80  

compare_next:  

    ; 比较 str_a 和 str_c(大小写不同)  

    cld  

    mov esi, str_a  

    mov edi, str_c  

    mov ecx, 7  

    repe cmpsb  

    jne not_equal_2  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, eq_msg  

    mov edx, eq_len  

    int 0x80  

    jmp exit  

not_equal_2:  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, ne_msg  

    mov edx, ne_len  

    int 0x80  

exit:  

    mov eax, 1  

    mov ebx, 0  

    int 0x80

字符串操作指令汇总

指令功能使用的寄存器
MOVSB复制字节:[EDI] = [ESI]ESI=源, EDI=目标, ECX=次数, DF=方向
MOVSW复制字(2 字节)同上
MOVSD复制双字(4 字节)同上
STOSB存字节:[EDI] = ALEDI=目标, AL=值, ECX=次数
LODSB取字节:AL = [ESI]ESI=源
CMPSB比较字节:[ESI] - [EDI]ESI=源, EDI=目标, ECX=次数
SCASB扫描字节:AL - [EDI]EDI=目标, AL=查找值, ECX=次数

大小写转换示例

实例

asm
; 文件路径:case_convert.asm  

; 将字符串中的小写字母转为大写  

section .data  

    msg db 'Hello, runoob! Welcome to Assembly.', 0xA  

    len equ $ - msg  

section .text  

    global _start  

_start:  

    ; 输出原始字符串  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, msg  

    mov edx, len  

    int 0x80  

    ; 转换:遍历字符串,小写 -> 大写  

    mov esi, msg                    ; 指向字符串开头  

    mov ecx, len                    ; 循环次数  

convert_loop:  

    mov al, [esi]                   ; 读取字符  

    cmp al, 'a'                     ; 是否 >= 'a'  

    jb  next_char                   ; 小于 'a',跳过  

    cmp al, 'z'                     ; 是否 <= 'z'  

    ja  next_char                   ; 大于 'z',跳过  

    ; 小写转大写:'a'(97) - 'A'(65) = 32  

    sub al, 32                      ; 转为大写  

    mov [esi], al                   ; 写回  

next_char:  

    inc esi  

    loop convert_loop  

    ; 输出转换后的字符串  

    mov eax, 4  

    mov ebx, 1  

    mov ecx, msg  

    mov edx, len  

    int 0x80  

    mov eax, 1  

    mov ebx, 0  

    int 0x80

运行结果:

$ nasm -f elf32 case_convert.asm -o case_convert.o
$ ld -m elf_i386 case_convert.o -o case_convert
$ ./case_convert
Hello, runoob! Welcome to Assembly.
HELLO, RUNOOB! WELCOME TO ASSEMBLY.

方向标志 DF 决定了字符串操作的方向:DF=0 时 ESI/EDI 递增(正向),DF=1 时递减(反向)。用 CLD 清零 DF,用 STD 置位 DF。在调用 C 函数或系统调用前应该确保 DF=0(cdecl 要求)。