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