返回

汇编 .asciz 字符串格式化:变量传递与打印详解

Linux

.asciz 字符串与格式化变量的处理

在内核开发或低级编程中,经常需要处理直接使用汇编语言定义的字符串。 .asciz 指令在某些汇编器中用于定义以 null 结尾的字符串。有时候,这些字符串内会包含格式化占位符,例如 %p,需要将变量值插入到这些占位符中,才能产生有意义的输出。问题是如何将变量值正确传递给打印函数,以处理字符串内的格式化占位符?本文就此问题进行探讨,并给出具体解决方案。

问题分析

这段汇编代码展示了典型的场景:一个中断处理例程中,使用一个带有 %p 占位符的 .asciz 字符串,尝试打印错误信息。问题在于,%p 究竟应该对应哪个值,以及汇编代码如何正确地传递这些值给打印函数? %p 在 C 语言 printf 及其衍生函数中常用来打印指针,这里也使用了相似的思路,预期在输出中显示的是地址值。

从代码中可以看到,pushl 16(%esp), pushl 24(%esp), pushl 32(%esp), pushl 40(%esp) 将栈中的值依次压栈,随后将字符串地址也压入栈,最后调用打印函数 early_printkprintk。这个机制说明了问题的核心,格式化字符串及其对应的参数都被放置在栈中,打印函数需要知道如何从栈中取出格式化字符串以及后续的参数。 格式化输出的关键就在于这些传递参数。

解决方案

解决此问题的方法主要在于如何保证栈中的数据与字符串内的占位符顺序对应。 核心就是需要仔细分析打印函数的调用约定,理解参数的传递方式。下面我们列举几种常见的情况并给出解决方法。

解决方案一: 栈中按序传递

最常见的模式就是按顺序把要输出的值压入栈,然后调用打印函数。打印函数内部解析格式字符串,并从栈中逐个读取对应值。

  • 原理: 此方案利用 x86 架构中函数调用的栈传递参数方式。 函数参数从右往左压入栈中,保证打印函数按照预期顺序读取。

  • 操作步骤:

    1. 按照 %p 占位符顺序,将需要输出的值压入栈。 示例中 16(%esp), 24(%esp), 32(%esp), 40(%esp) 就是事先压入栈中的地址值, pushl $int_msg.asciz 字符串的地址压栈,然后调用打印函数。
    2. 打印函数 printkearly_printk 的实现,需要理解如何读取格式化字符串和参数。 该函数内部应该包含相应的格式化解析逻辑。
  • 代码示例(部分):

ignore_int:
  ... 
  pushl 16(%esp) ; 第一个 %p 的值
  pushl 24(%esp) ; 第二个 %p 的值
  pushl 32(%esp) ; 第三个 %p 的值
  pushl $int_msg   ; 格式化字符串的地址
  call printk       ; 调用打印函数
  ...
  • 注意: 此方案中,确保压入栈的数值类型正确匹配 printf%p 格式,比如压入的是内存地址而不是普通整数。另外 printk函数的参数读取机制,通常采用栈指针和格式化字符串来处理参数。

解决方案二: 特定寄存器传递 (非本示例情景,但为通用方案补充)

某些情况下,处理器约定使用特定寄存器传递函数参数,而不是全部压入栈中。

  • 原理: 部分调用约定使用寄存器传递参数可以加速函数调用。此方案要求打印函数有特定实现来从约定寄存器读取参数。

  • 操作步骤:

    1. 将格式化字符串地址加载到特定寄存器(比如 x86_64 架构中可能是 rdi 寄存器)
    2. 将后续要格式化的变量值加载到其他的特定寄存器(如 rsi, rdx, rcx 等寄存器,数量与格式符的数量一致)。
    3. 调用打印函数。
  • 代码示例 (x86_64 为例,与问题无关)

 mov $format_string, %rdi 
 mov %rax, %rsi      ;  假设 rax 保存第一个参数值
 mov %rbx, %rdx      ;  假设 rbx 保存第二个参数值
 call print_function
  • 注意: 这个解决方案特定于系统架构和所使用的调用约定。 实际中必须了解你的系统使用的是什么ABI和寄存器规则。

解决方案三:全局变量或特定结构体 (通常不推荐,为理解参数传递补充)

如果系统参数比较复杂或者不希望通过栈传递,可以考虑使用全局变量或者事先准备好的结构体存放,并将其地址传递给打印函数。

  • 原理 : 直接传递数据的地址,或者全局存储,减少栈的维护工作,这在非常早期内核中,可以用于传递一些打印函数急需的数据。

  • 操作步骤 :

    1. 事先把参数的值存储在预定的内存区域(比如全局变量或结构体)。
    2. 将内存地址传给打印函数。
    3. 打印函数中,根据预定的内存结构读取参数。
  • 代码示例 (概念示例)

 data_address:.dword 0 ; 假设的参数存储区地址

 set_parameters: ; 将数据填入 data_address指向的位置
 mov $var1, data_address   
 mov data_address,%rdi     ; 将数据地址放入寄存器传递
 push $format_string
 call print_function
  • 注意: 全局变量易产生状态污染,线程安全是潜在问题,结构体传地址也有风险。通常不作为主流选择。

额外的安全建议

在汇编代码中使用格式化字符串时,一定要小心潜在的安全风险。

  • 格式字符串漏洞: 避免将用户输入作为格式字符串传递给打印函数,以防格式化字符串漏洞攻击。使用固定的格式字符串和受信任的输入,可以避免这类问题。
  • 类型安全: 确保传递的参数与格式化字符串中的占位符匹配。类型不匹配可能会导致程序崩溃或产生意外行为。 例如 printf 中的 %p 需要是 void*类型的指针变量,%d需要int 型数据。 错误传递其它类型可能会出现未定义行为。
  • 栈溢出风险: 如果在栈上过度分配,参数数目错误或传递了超大的字符串时,存在栈溢出的风险。确保栈空间足够。

理解汇编代码中字符串和格式化输出原理至关重要。选择合适的参数传递方式取决于特定的系统架构和调用约定。本文提供了一系列的通用方案,开发人员应结合实际环境谨慎选择,才能避免产生难以追踪的程序问题。