memcpy vs memcpy_toio

0x00 背景

环境:Linux kernel 5.15.78 aarch64 armv8
现象:使用vmap()以nocache的形式映射一段物理地址,使用memcpy()往其中写入数据,在写入最后64字节时发生如下错误

Unable to handle kernel paging request at virtual address ffffffc01a13567c

确认并未发生溢出,且使用memcpy_toio()或以cache形式映射则不会有这个问题。
网上搜了一圈没发现很好的解释,遂read the fxxking source code,趁热记录下来。
本文主要探讨两个问题:
1.memcpy()及memcpy_toio()的差别;
2.何时该使用memcpy()或memcpy_toio();

0x01 memcpy

首先来看一下memcpy()的实现,在__HAVE_ARCH_MEMCPY未定义的情况下实现如下

lib/string.c
#ifndef __HAVE_ARCH_MEMCPY
/**
 * memcpy - Copy one area of memory to another
 * @dest: Where to copy to
 * @src: Where to copy from
 * @count: The size of the area.
 *
 * You should not use this function to access IO space, use memcpy_toio()
 * or memcpy_fromio() instead.
 */
void *memcpy(void *dest, const void *src, size_t count)
{
	char *tmp = dest;
	const char *s = src;

	while (count--)
		*tmp++ = *s++;
	return dest;
}
EXPORT_SYMBOL(memcpy);
#endif

这是一个通用的memcpy()实现,每次循环只copy一个字节。函数说明了在访问IO space时应该用memcpy_toio()或memcpy_fromio()来替代。但是WHY???这里并没有说明原因,先接着往下看。
通常为了更高的效率,memcpy()都由架构自己实现,arm64下的memcpy代码片段如下

arch/arm64/lib/memcpy.S
/* ... */
#define A_l		x6
#define A_lw	w6
#define A_h		x7
#define B_l		x8
#define B_lw	w8
#define B_h		x9
#define C_l		x10
#define C_lw	w10
#define C_h		x11
#define D_l		x12
#define D_h		x13
/* ... */
L(copy128):
	ldp	E_l, E_h, [src, 32]
	ldp	F_l, F_h, [src, 48]
	cmp	count, 96
	b.ls	L(copy96)
	ldp	G_l, G_h, [srcend, -64]
	ldp	H_l, H_h, [srcend, -48]
	stp	G_l, G_h, [dstend, -64]
	stp	H_l, H_h, [dstend, -48]
L(copy96):
	stp	A_l, A_h, [dstin]
	stp	B_l, B_h, [dstin, 16]
	stp	E_l, E_h, [dstin, 32]
	stp	F_l, F_h, [dstin, 48]
	stp	C_l, C_h, [dstend, -32]
	stp	D_l, D_h, [dstend, -16]
	ret
/* ... */

arm64下最多可以一次拷贝128个字节,同前者相比,效率可谓大大提升。

0x02 memcpy_toio

接下来看一下memcpy_toio()的实现

arch/arm64/kernel/io.c
void __memcpy_toio(volatile void __iomem *to, const void *from, size_t count)
{8字节对齐则一个个字节copy
	while (count && !IS_ALIGNED((unsigned long)to, 8)) {
		__raw_writeb(*(u8 *)from, to);
		from++;
		to++;
		count--;
	}

	对齐则每次copy8字节
	while (count >= 8) {
		__raw_writeq(*(u64 *)from, to);
		from += 8;
		to += 8;
		count -= 8;
	}

	剩余不足8字节部分一个个字节copy
	while (count) {
		__raw_writeb(*(u8 *)from, to);
		from++;
		to++;
		count--;
	}
}
EXPORT_SYMBOL(__memcpy_toio);

效率比通用memcpy()高,但和架构下的实现相比还是有所差距。

0x03分析

每次发生问题都是在copy最后64字节的时候,来看看memcpy()这时候在干嘛

	/* Write the last iteration and copy 64 bytes from the end.  */
L(copy64_from_end):
	ldp	E_l, E_h, [srcend, -64]
	stp	A_l, A_h, [dst, 16]
	ldp	A_l, A_h, [srcend, -48]
	stp	B_l, B_h, [dst, 32]
	ldp	B_l, B_h, [srcend, -32]
	stp	C_l, C_h, [dst, 48]
	ldp	C_l, C_h, [srcend, -16]
	stp	D_l, D_h, [dst, 64]
	stp	E_l, E_h, [dstend, -64]<--执行到这句出错
	stp	A_l, A_h, [dstend, -48]
	stp	B_l, B_h, [dstend, -32]
	stp	C_l, C_h, [dstend, -16]
	ret

memcpy()会提前计算好dstend(dst+conut),即目标地址加上要写的大小。然后从dstend往前64字节开始copy最后64字节的内容。
其中E_l,E_h分别为寄存器x14,x15,都是64bit的寄存器。
这里就引发一个问题,如果dstend不是64bit对齐的话,就会引发一次非对齐访问。
猜测正是这个非对齐访问造成了错误。尝试将写入大小改为64bit对齐,果然不会出错了。而使用memcpy_toio()不会出错也正是因为最后非对齐的字节memcpy_toio()是采取byte by byte的方式进行copy的,不存在非对齐访问的问题。到这里算是解决了memcpy()与memcpy_toio()的问题,还剩另一个问题,为什么以cache形式映射的内存可以进行非对齐访问呢?ARM官方给出了说明https://developer.arm.com/documentation/ka004708/latest。

0x04结论

1.memcpy()比memcpy_toio()具有更高的效率;
2.memcpy()要求copy size对齐;
3.copy size对齐时两者可以互换;
4.copy size非对齐时,若不带缓存,则使用memcpy_toio(),若带缓存,则可使用memcpy();

你可能感兴趣的:(linux,嵌入式,c语言)