由浅入深C系列四:memset/memcpy源码分析,为什么这二个函数的目的字符串在前面,源字符串在后面?

简介: memset/memcpy源码分析,为什么这二个函数的目的字符串在前面,源字符串在后面?

困惑起源

memset/memcpy是我们常用的库函数, 有没有想过,为什么都是dest在前面,src在后面?:)

/* Copy N bytes of SRC to DEST.  */
extern void *memcpy (void *__restrict __dest, const void *__restrict __src,
             size_t __n) __THROW __nonnull ((1, 2));
/* Set N bytes of S to C.  */
extern void *memset (void *__s, int __c, size_t __n) __THROW __nonnull ((1));

读源码,解困惑

精彩的部分都在源码里面加了说明,先总体说下。在以下源码中,ax, dx,分别对应void *__restrict __dest, const void *__restrict __src,而cx对应size_t __n。这样就比较明显了:
1、ax进了di(目的地址寄存器),dx进了si(源地址寄存器),movsl从si向di复制内容
2、cx是用于rep的程序计数器。当rep重复 movsl指令时,自动从cx中减1。

聪明的同学可能要问,我把ax/dx换一下行不行,是不是就可以把src放在前面,dest放在后面了?理论上是这样的,不过从C89到C11这么多年了,就不要去改了吧!:)

其实真相只有一个:遵循fastcall的原则和调用约定,从右开始不大于4字节的参数放入CPU的ecx,edx,eax寄存器,其余参数从右向左入栈,从汇编实现上来看,也是遵循了这样一个调用约定。

以下是memcpy的代码,里面有2个精彩的部分,同学们可以好好体会一下。

SYM_FUNC_START_NOALIGN(memcpy) // 以下代码来源于\linux-6.1-rc1\arch\x86\boot\copy.S
    pushw    %si       // 保存好调用前别人的现场,不添麻烦
    pushw    %di       // 同上
    movw    %ax, %di  // void *__restrict __dest
    movw    %dx, %si  // const void *__restrict __src
    pushw    %cx       // 先存起来,后面有交代
    shrw    $2, %cx   // size_t __n,注意!精彩的来了,由于是movsl是4个字节,所以,对cx右移2次,相当于除4
    rep; movsl        // 第一次循环,发挥最大性能,4字节一起上,执行movsl并循环cx次(上面做了整除4,精彩吧!)
    popw    %cx       // 没有和4个字节对齐的剩余的部分也要处理,不然就漏掉了:)
    andw    $3, %cx   // andw求和取余,(3)=0x0011,为啥是3?(4)=0x0100,明白了吧,只要后面的就行
    rep; movsb        // 再来一次循环,这次movsb,按1字节复制。
    popw    %di       // fastcall,规矩做事,清理干净,恢复现场
    popw    %si       // 同上
    retl
SYM_FUNC_END(memcpy)

以下是memset的代码,其逻辑和上面的差不多。可以参考上面的批注理解。

SYM_FUNC_START_NOALIGN(memset)
    pushw    %di
    movw    %ax, %di
    movzbl    %dl, %eax   // 1个字节太慢,把eax用起来,先把dl的低位字节放到eax,等下面imull后,4个字节一起飞
    imull    $0x01010101,%eax   //注意,也是精彩的地方,把入参中用于设置的1个字节copy4份,为stosl准备加速复制
    pushw    %cx
    shrw    $2, %cx
    rep; stosl
    popw    %cx
    andw    $3, %cx   // 和memcpy一样,不多说,主要为了收尾处理剩下的不能补4整除的部分
    rep; stosb
    popw    %di
    retl
SYM_FUNC_END(memset)

结论

符合fastcall的调用约定本来就是从右向左的,对吧。(前面已经说明清楚了,凑一个结论吧:))

相关文章
|
2月前
|
存储 C语言
【C语言基础考研向】10 字符数组初始化及传递和scanf 读取字符串
本文介绍了C语言中字符数组的初始化方法及其在函数间传递的注意事项。字符数组初始化有两种方式:逐个字符赋值或整体初始化字符串。实际工作中常用后者,如`char c[10]="hello"`。示例代码展示了如何初始化及传递字符数组,并解释了为何未正确添加结束符`\0`会导致乱码。此外,还讨论了`scanf`函数读取字符串时忽略空格和回车的特点。
|
3月前
|
C语言
【C语言篇】字符和字符串以及内存函数详细介绍与模拟实现(下篇)
perror函数打印完参数部分的字符串后,再打印⼀个冒号和⼀个空格,再打印错误信息。
59 0
|
3月前
|
存储 安全 编译器
【C语言篇】字符和字符串以及内存函数的详细介绍与模拟实现(上篇)
当然可以用scanf和printf输入输出,这里在之前【C语言篇】scanf和printf万字超详细介绍(基本加拓展用法)已经讲过了,这里就不再赘述,主要介绍只针对字符的函数.
50 0
|
C语言
C语言:写一个函数返回参数二进制中 1 的个数(三种思路)-1
思路一:使用 %2 和 /2 取出每一位并判断 总体思路: (一). 创建函数,参数要设置成无符号整数,设置计数器计算1的个数 (二). 使用 while循环 循环判断二进制每一位, 使用 %2 判断最低位是否为 1, 使用 /2 去掉判断了的最低位,下次循环开始判断新的最低位
 C语言:写一个函数返回参数二进制中 1 的个数(三种思路)-1
|
存储 Serverless
strlen函数解析与模拟实现
strlen函数解析与模拟实现
strlen函数解析与模拟实现
模拟实现库函数strcat--将源字符串的副本追加到目标字符串(理解内存重叠问题)
模拟实现库函数strcat--将源字符串的副本追加到目标字符串(理解内存重叠问题)
实现一个函数,使用指针连接两个字符串。函数输入: 两个源字符串的指针,目的字符串的指(C++指针练习4)
实现一个函数,使用指针连接两个字符串。函数输入: 两个源字符串的指针,目的字符串的指(C++指针练习4)
实现一个函数,使用指针连接两个字符串。函数输入:两个源字符串的指针,目的字符串的指针。
实现一个函数,使用指针连接两个字符串。函数输入:两个源字符串的指针,目的字符串的指针。
|
C语言
探究一下:使用memcpy函数能不能自己拷贝自己
探究一下:使用memcpy函数能不能自己拷贝自己
210 0
探究一下:使用memcpy函数能不能自己拷贝自己