返回

x64 Linux 汇编: 可变长字符数组的动态处理

Linux

x64 Linux 汇编中可变长度字符数组的处理

在 x64 Linux 汇编编程中,处理可变长度字符数组是一个常见的问题。固定大小的缓冲区往往不够灵活,而.bss段的大量声明则显得笨拙。如何有效地分配和管理字符数组,同时避免常见的陷阱呢? 本文将探讨几种可能的解决方案。

问题根源

汇编语言不像高级语言那样提供内置的字符串类型和自动内存管理。因此,动态创建和调整字符数组的存储空间需要手动完成。 常规做法使用预先分配的固定大小缓冲区(.bss段),但这限制了字符串的长度,并且当需要连接字符串时,需要精心管理缓冲区的大小和内存复制操作。 使用栈空间尝试动态扩展面临的问题是如何有效地回收空间,且在多个函数调用之间难以维持。

解决方案:利用 mmap 进行动态内存分配

mmap 系统调用允许从操作系统内核分配一块虚拟内存区域。这种方法具有高度的灵活性,可以动态地分配所需大小的内存。与在栈上分配内存不同,mmap 分配的内存位于堆上,生命周期更长,更适合存储需要跨函数使用的字符串。

实现步骤:

  1. 使用 mmap 系统调用分配内存。你需要指定 MAP_ANONYMOUSMAP_PRIVATE 标志以获取一块私有匿名映射。 还需要提供所需内存的大小。

  2. 将分配的内存地址保存到寄存器或内存位置,以便后续使用。

  3. 进行字符串操作(复制,连接等),确保不超过已分配的内存大小。必要时重新分配更大的内存块。

  4. 完成操作后,使用 munmap 系统调用释放内存。

代码示例:

; Syscall numbers
%define SYS_MMAP 9
%define SYS_MUNMAP 11
%define SYS_EXIT 60

; mmap flags
%define MAP_ANONYMOUS 0x20
%define MAP_PRIVATE 0x02

;Prot flags
%define PROT_READ 0x1
%define PROT_WRITE 0x2

section .data
    message db "Hello, world!", 0 ;example string

section .text
    global _start

_start:
    ; Allocate memory using mmap
    mov rax, SYS_MMAP       ; sys_mmap
    mov rdi, 0              ; addr (NULL for kernel to choose)
    mov rsi, 4096           ; length (page size - common practice)
    mov rdx, PROT_READ | PROT_WRITE  ; prot (read/write)
    mov r10, MAP_ANONYMOUS | MAP_PRIVATE ; flags (anonymous private mapping)
    mov r8, -1              ; fd (-1 for anonymous mapping)
    mov r9, 0               ; offset (0)
    syscall
    
    ; Check for errors (rax contains the pointer to the new allocation on success or -errno on failure)
    cmp rax, 0 ;error value
    jl error_handler ; handle error. Exit program as exemple
    
    ; Save the allocated memory address
    mov r15, rax            ; Store allocated memory address in r15

    ;Copy string into the mmap'ed section of memory. (replace rsi and rdi value with data section and mmap alloc respectively)
    mov rsi, message
    mov rdi, r15
    call string_copy ;Simple loop to copy the string from a source to a dest buffer
    
    ;Print the copied string

    mov rsi, r15  ; rsi should point to our buffer in memory
    call .print ; print routine (Given code)

    ;Deallocate the memory
    mov rax, SYS_MUNMAP
    mov rdi, r15 ; address obtained with mmap call
    mov rsi, 4096; length (same length that we asked to mmap).
    syscall

    ; Exit the program
    mov rax, SYS_EXIT
    xor rdi, rdi
    syscall

;----------------------------;

string_copy:
    push rbp
    mov rbp, rsp

.copy_loop:
    mov al, [rsi]
    cmp al, 0
    je .copy_end
    mov [rdi], al
    inc rsi
    inc rdi
    jmp .copy_loop
.copy_end:
    pop rbp
    ret

;----- Print Routine (same than give code) ----------

.print:
    push rcx                    ; save increment if used in another function
    xor rcx, rcx                ; reset increment to 0

.print_loop:
    cmp byte [rsi + rcx], 0     ; check if null
    je .print_done              ; if yes print the value beacause end of string
    inc rcx                     ; else increment 
    jmp .print_loop             ; and restart the operation

.print_done:
    mov rdx, rcx                ; pass the string size as parameter syscall sys_write
    mov rax, 1                  ; prepare syscall sys_write function
    mov rdi, 1                  ; prepare syscall stdout output
    syscall                     ; print the string
    pop rcx                     ; get the old increment value to handle return
    ret

;-------------ERROR HANDLER----------
error_handler:
    ;Basic error handling logic. Print error message (if applicable) and terminate execution
    mov rax, SYS_EXIT
    mov rdi, 1
    syscall

安全建议:

  • 始终检查 mmap 的返回值,以确保内存分配成功。
  • 确保在使用完 mmap 分配的内存后,使用 munmap 及时释放内存,避免内存泄漏。
  • 对写入 mmap 区域的数据量进行限制,以防止缓冲区溢出。可以使用 realloc 的思路进行内存扩展,需要复制原有数据。

brk 系统调用进行内存管理

brksbrk 系统调用直接操纵程序的堆空间。brk 设置堆的结束地址,sbrk 增加堆的大小。 虽然 mmap 更常用,但理解 brk 的工作方式也有助于更深入地了解内存管理。

实现步骤:

  1. 调用 brk(0) 获取当前堆的结束地址。
  2. 计算所需的额外空间大小。
  3. 使用 brk(new_break) 调整堆的大小,其中 new_break 是新的堆结束地址。
  4. 将数据写入新分配的堆空间。
  5. 在程序结束时,理论上可以调用 brk 恢复到初始堆大小(虽然实际中不常这样做,操作系统在进程退出时会自动回收内存)。

代码示例:

section .data
    message db "Hello, brk!", 0

section .text
    global _start

_start:
    ; Get current break (end of heap)
    mov rax, 12   ; SYS_brk
    mov rdi, 0    ; Request current break address
    syscall

    ; Save current break
    mov r12, rax

    ; Calculate required size (string length + null terminator)
    ; In real applications use `strlen` function or known string length instead of hardcoding values

    ; Increase heap size to accomodate new String
    mov rax, 12   ; SYS_brk
    add rax, 14    ;Increase the rax by our String size
    mov rdi, rax ;new break pointer
    syscall

    cmp rax, -ENOMEM; check return value. Jump to error if negative
    jl error_handler

    ; Copy string into newly allocated space (replace source and destination indexes)
    mov rsi, message
    mov rdi, r12
    call string_copy;Same routine

    ; Print String

    mov rsi, r12 ; pass the start address where data where copied
    call .print; print routine (given code)
    
    ;Exit program
    mov rax, 60
    xor rdi, rdi
    syscall

; String copy function
string_copy:
    push rbp
    mov rbp, rsp

.copy_loop:
    mov al, [rsi]
    cmp al, 0
    je .copy_end
    mov [rdi], al
    inc rsi
    inc rdi
    jmp .copy_loop
.copy_end:
    pop rbp
    ret

;---print routine

.print:
    push rcx                    ; save increment if used in another function
    xor rcx, rcx                ; reset increment to 0

.print_loop:
    cmp byte [rsi + rcx], 0     ; check if null
    je .print_done              ; if yes print the value beacause end of string
    inc rcx                     ; else increment 
    jmp .print_loop             ; and restart the operation

.print_done:
    mov rdx, rcx                ; pass the string size as parameter syscall sys_write
    mov rax, 1                  ; prepare syscall sys_write function
    mov rdi, 1                  ; prepare syscall stdout output
    syscall                     ; print the string
    pop rcx                     ; get the old increment value to handle return
    ret
;-------------ERROR HANDLER----------
error_handler:
    ;Basic error handling logic. Print error message (if applicable) and terminate execution
    mov rax, 60
    mov rdi, 1
    syscall

安全建议:

  • 需要进行错误处理以防止请求过大的堆空间。
  • 在多线程程序中使用 brk 需要进行适当的同步,因为堆是所有线程共享的资源。

结论

处理 x64 Linux 汇编中的可变长度字符数组需要理解内存管理的底层机制。mmap 提供了更灵活和常用的方法来动态分配内存,而理解 brk 有助于更深入地了解堆的工作方式。选择哪种方法取决于具体的应用场景和需求。始终记住,仔细的内存管理和安全措施是编写健壮和安全汇编代码的关键。