__do_strncpy_from_user()源码分析

本文详细解析了64位系统中strncpy_from_user函数的工作原理。该函数通过内嵌汇编的方式实现从用户空间复制字符串到内核空间,确保在复制过程中能够正确处理异常并保护内核稳定。

  在用户层发起系统调用时,有时需要向内核传递字符串。内核通常不会直接操作用户传入的字符串,而是先拷贝到内核缓冲区中。因为是用户态传入的指针,并不能保证在拷贝的过程中不会发生异常,所以内核必须保证这种异常不会影响到内核的运行。内核通常会使用copy_from_user()或者strncpy_from_user()等类似的函数来执行拷贝操作。我们这里以strncpy_from_user()(64位系统)为例进行分析。strncpy_from_user()函数最终是调用__do_strncpy_from_user宏来完成拷贝,这个宏是通过内嵌汇编的方式定义的,源码如下:

/*
* Copy a null terminated string from userspace.
*/
#define __do_strncpy_from_user(dst,src,count,res)               \
do {                                       \
    long __d0, __d1, __d2;                           \
    might_fault();                               \
    __asm__ __volatile__(                           \
        "    testq %1,%1\n"                       \
        "    jz 2f\n"                       \
        "0:    lodsb\n"                       \
        "    stosb\n"                       \
        "    testb %%al,%%al\n"                   \
        "    jz 1f\n"                       \
        "    decq %1\n"                       \
        "    jnz 0b\n"                       \
        "1:    subq %1,%0\n"                       \
        "2:\n"                               \
        ".section .fixup,\"ax\"\n"                   \
        "3:    movq %5,%0\n"                       \
        "    jmp 2b\n"                       \
        ".previous\n"                           \
        _ASM_EXTABLE(0b,3b)                       \
        : "=&r"(res), "=&c"(count), "=&a" (__d0), "=&S" (__d1),       \
          "=&D" (__d2)                           \
        : "i"(-EFAULT), "0"(count), "1"(count), "3"(src), "4"(dst) \
        : "memory");                           \
} while (0)
  首先看下面的指令:  

        testq %1,%1\n"
            jz 2f\n
  test指令都比较熟悉,就是对两个操作数执行AND操作,然后根据结果设置ZF、SF、PF寄存器,AND操作的结果会被丢弃。这里的两个操作数都是%1,也就是输出参数count。假设count为0的话,两个count值执行AND操作的结果也是0,此时ZF寄存器会被置位;同理,如果count不为0,则ZF寄存器就不会置位。如果ZF寄存器置位的话,会跳转到符号2的位置,也就是说要拷贝的字符串长度count为0,此时不执行任何拷贝操作,直接返回。这里有两个地方需要注意,这里的“f“的意思是forward,也就是向前跳转,f可以省略,默认就是向前跳转;第二个地方是,跳转到符号2的位置后,不会执行.section .fixup之后的代码,后面再说这个段。
  如果count不为0,则从符号0处开始执行。lodsb指令是将地址为DS:(E)SI的字节加载到寄存器AL中,同时si寄存器会自动增加或减小1,取决于方向标志位DF。64位系统下,lodsb是加载地址为%rsi的字节到AL中。src是作为输入参数,和编号为3的参数使用相同的寄存器或内存,也就是和__d1(编号是从0开始)使用相同的寄存器或内存。__d1的约束是"=&S",其中"&"约束只能用于Output操作表达式,向GCC表明"GCC不得为任何Input操作表达式分配与此Output操作表达式相同的寄存器";"="约束说明当前的操作表达式是Write-Only的,只能用作输出,而不能作为输入;"S"约束是寄存器约束,表示要使用寄存器%rsi/%esi/%si。因为src和__d1使用相同的寄存器约束,所以src的地址会使用%rsi寄存器来存储。这里lodsb是将src指向的字符串中的字符加载到寄存器AL中,注意,这里lodsb使用的是寄存器si的值作为地址值,所以递增的是si,不是src指针值。
  现在是把字符加载到寄存器AL,接着使用stosb指令将AL寄存器存储的字符存储到dst执行的内存中。stosb指令的作用是将AL的内容存储到地址为ES:(E)DI的字节处,同时di寄存器会自动增加或减小1,取决于方向标志位DF。64位系统下,将AL寄存器的内容存储到地址为%rdi的字节处。同src的原理一样,dst使用的是和__d2相同的寄存器%rdi,也就是将内容存储到dst中。
  lodsb和stosb两个指令结合起来就是把字符从src拷贝到dst中,每次都是只拷贝一个字符。
  在拷贝完一个字符后,会使用下面的指令来来检查拷贝的字符是否是空字符,如下所示:
    testb %%al,%%al
            jz 1f
  在内联汇编中,要访问寄存器要使用两个%符号,如果是空字符的话,则跳转到符号1的位置,开始计算拷贝的字符的数量,后面再讨论。如果不是空字符,则执行下面的指令:

        decq %1
            jnz 0b
  使用decq指令将编号为1的参数(初始值为count)减1。如果编号为1的参数的值不为0,则跳转到符号0的位置,重复上面拷贝字符的操作。编号为1的参数值如果为0,则说明已经拷贝了最大数量的字符,此时可以结束执行,到符号1的位置计算拷贝的字符数量,然后返回。这里的0b中字符"b"的意思是backward,就是向后跳转,因为默认情况是向前跳转,所以这里的b不能省略,因为符号0在这条指令前边。
  符号1处的指令如下所示:

"1:    subq %1,%0\n"
  这条指令相当于是执行%0=%0-%1。编号为0的参数是res,但是在前边我们没有看到对res初始化的地方,res本身就是用来存储结果的,那res的初始值是多少呢?我们首先看输入参数"0"(count),也就是编号为6的参数,这个输入参数使用编号为0的参数相同的寄存器约束,也就是res所使用的寄存器。所以res的值和编号为6的参数相同,也就是说res的初始值就是要拷贝的字符的最大数量count,而每拷贝一个字符编号为1的参数(初始也为count)值都会减1,所以%0-%1就是已经拷贝的字符数量,结果存储在%1中,也就是res中。到这里,拷贝操作顺利执行完成。
  接下来看.fixup段之后的的指令,如下所示:
    .section .fixup,\"ax\"
        3:    movq %5,%0
        "    jmp 2b
        ".previous
        _ASM_EXTABLE(0b,3b) 
 其中_ASM_EXTABLE宏是向异常表中插入一项(这个宏比较简单,这里就不替换了),在符号0处的代码发生异常时,会跳转到符号3处处理异常,也就是.fixup段中的指令。
 .section .fixup之后的代码会被插入到.fixup段中,“ax”用来表示这段代码必须被加载到内存中,并且其中包含可执行的代码。如果符号0处的指令引发了Page Fault异常,会跳转到.fixup段中处理。这里的处理就是将res的值设置为-EFAULT,然后跳转到符号2处返回。

<think>嗯,用户询问的是CUTIL_STR_STRNCPY函数的作用。这是一个特定库中的字符串操作函数,需要从名称和常见实现模式来推断功能。 从函数名拆解看,"CUTIL"应该是自定义工具库的前缀,"STR"表明属于字符串处理模块,"STRNCPY"明显对应标准库的strncpy。但加了前缀说明是定制版本,可能解决标准函数的某些缺陷。 标准strncpy有两个痛点:不会自动添加终止符,以及填充空字节的性能问题。安全编程中常需要封装改进版。推测CUTIL_STR_STRNCPY的核心改进点应该在这两方面: - 保证目标缓冲区始终以'\0'结尾 - 可能通过先清零内存避免无关数据残留 - 返回值设计可能更合理 用户可能是嵌入式开发者或安全敏感系统的程序员,遇到标准函数不能满足需求的情况。这类封装函数在物联网设备开发中很常见,用户应该需要明确的行为定义。 接下来要考虑用户可能的后续疑问:参数细节、内存处理原理、线程安全性等。特别要提醒缓冲区溢出风险,因为即使用这个函数,目标缓冲区大小管理仍是关键。 相关问题需要覆盖函数设计原理、适用场景和替代方案。第一个问题聚焦参数定义,因为大小参数传递方式直接影响安全性;第二个针对与标准库区别,这是用户选择时的决策点;第三个延伸讨论空字符处理逻辑;第四个考虑特殊需求场景;最后补充错误处理机制,这对健壮性很重要。 </think> `CUTIL_STR_STRNCPY` 函数(通常出现在自定义的 C 工具库中,如 `cutil` 库)的主要作用是**安全地复制一个字符串到目标缓冲区,并确保目标字符串以空字符 (`'\0'`) 终止**。它是标准 C 库函数 `strncpy` 的一个更安全、更易用的封装或改进版本。 它的核心作用和工作原理如下: 1. **限制复制长度:** 和 `strncpy` 一样,它接受一个参数指定目标缓冲区 `dest` 的最大可用大小 `dest_size`(通常包括结尾的空字符位置)。 2. **保证空字符终止:** 这是它与 `strncpy` 最关键的区别。无论源字符串 `src` 的长度如何,它**总是**会在目标缓冲区 `dest` 的**有效范围内**放置一个终止空字符 (`'\0'`)。这消除了使用 `strncpy` 时常见的错误:当 `src` 长度大于等于 `dest_size` 时,`strncpy` 不会添加终止符,导致目标缓冲区不是有效的 C 字符串。 3. **防止缓冲区溢出:** 通过严格限制最多复制 `dest_size - 1` 个字符(为终止符预留一个位置),它从根本上避免了目标缓冲区溢出的风险。 4. **处理源字符串过长:** 如果源字符串 `src` 的长度(包括其自身的终止符)大于或等于 `dest_size`: * 它会将 `src` 的前 `dest_size - 1` 个字符复制到 `dest`。 * 然后在 `dest` 的第 `dest_size - 1` 个位置(即缓冲区的最后一个可用位置)**强制写入一个空字符 (`'\0')**。 * 结果是 `dest` 包含一个被截断但**保证以 `'\0'` 结尾**的字符串。 5. **处理源字符串较短:** 如果 `src` 的长度(包括其终止符)小于 `dest_size`: * 它会完整复制 `src`(包括其终止符)到 `dest`。 * 目标缓冲区中 `src` 终止符之后的空间内容**通常保持不变**(这与 `strncpy` 会用 `'\0'` 填充剩余空间的行为不同,后者有时是低效的)。某些实现可能会选择清零剩余空间,但保证终止是核心。 6. **返回值:** 通常返回指向目标字符串 `dest` 的指针(类似于 `strcpy` 和 `strncpy`)。有些实现可能返回复制的字符数或错误状态。 **总结来说,`CUTIL_STR_STRNCPY` 的核心价值在于:** * **安全性:** 通过长度限制防止缓冲区溢出。 * **可靠性:** 总是生成一个有效的、以空字符结尾的 C 字符串,避免了 `strncpy` 可能产生非终止字符串的陷阱。 * **易用性:** 开发者不再需要手动检查 `strncpy` 后是否需要添加终止符,减少了错误。 **函数签名通常类似于:** ```c char *CUTIL_STR_STRNCPY(char *dest, const char *src, size_t dest_size); ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值