由浅入深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的调用约定本来就是从右向左的,对吧。(前面已经说明清楚了,凑一个结论吧:))

相关文章
|
4月前
|
存储 C语言
【C语言基础考研向】10 字符数组初始化及传递和scanf 读取字符串
本文介绍了C语言中字符数组的初始化方法及其在函数间传递的注意事项。字符数组初始化有两种方式:逐个字符赋值或整体初始化字符串。实际工作中常用后者,如`char c[10]="hello"`。示例代码展示了如何初始化及传递字符数组,并解释了为何未正确添加结束符`\0`会导致乱码。此外,还讨论了`scanf`函数读取字符串时忽略空格和回车的特点。
122 8
|
8月前
|
程序员 C语言
【C语言】函数----函数的分类、库函数详解(strcpy、memset)、自定义函数的实现(找较大值、交换两个数)
【C语言】函数----函数的分类、库函数详解(strcpy、memset)、自定义函数的实现(找较大值、交换两个数)
54 0
|
存储 编译器 C语言
初识C语言指针:内存,字节划分,地址存储,简单使用,指针大小
初识C语言指针:内存,字节划分,地址存储,简单使用,指针大小
|
8月前
|
存储 C语言
c语言中strlen与sizeof的区别(指针面试题详解帮你深度区分!)
c语言中strlen与sizeof的区别(指针面试题详解帮你深度区分!)
101 0
全网最详细的sizeof运算和strlen函数讲解(通过多种数据类型举例)
全网最详细的sizeof运算和strlen函数讲解(通过多种数据类型举例)
模拟实现库函数strcat--将源字符串的副本追加到目标字符串(理解内存重叠问题)
模拟实现库函数strcat--将源字符串的副本追加到目标字符串(理解内存重叠问题)
学C的第五天(初识指针,内存产生内存单元地址过程,指针函数介绍和其大小;结构体补充)-1
13*指针: 13.1 -- 内存: 内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的。 【32位操作系统的内存由32根地址线产生的高低电位(1/0), 划分为2的32次方个地址(编号\指针),一个地址为一个字节, 等于8比特。】
实现一个函数,使用指针连接两个字符串。函数输入: 两个源字符串的指针,目的字符串的指(C++指针练习4)
实现一个函数,使用指针连接两个字符串。函数输入: 两个源字符串的指针,目的字符串的指(C++指针练习4)
实现一个函数,使用指针连接两个字符串。函数输入:两个源字符串的指针,目的字符串的指针。
实现一个函数,使用指针连接两个字符串。函数输入:两个源字符串的指针,目的字符串的指针。
|
C语言
探究一下:使用memcpy函数能不能自己拷贝自己
探究一下:使用memcpy函数能不能自己拷贝自己
232 0
探究一下:使用memcpy函数能不能自己拷贝自己

热门文章

最新文章