WIP: Linux string.c memcpy等的优化

通用实现

如果在各个 arch 下有相应的实现,则会定义一个 __HAVE_ARCH_XXX 的宏,此时这里就不会定义通用的实现版本,符号来自 相应的 arch 目录。

// 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
...

架构优化

实现

// arch/arm64/lib/memcpy.S
...
#include “copy_temlate.S”
...

使用

// arch/arm64/include/asm/string.h
...
#define __HAVE_ARCH_MEMCPY
extern void *memcpy(void *, const void *, __kernel_size_t);
extern void *__memcpy(void *, const void *, __kernel_size_t);
...

在各个 arch 下如果有相应的实现,则会定义一个 __HAVE_ARCH_XXX 的宏。
include/linux/string.h 中会包含该 asm/string.h,如果没有定义优化的函数则需要声明一下,否则无需再次声明。

通常包含 linxu/string.h 即可使用相应的接口。

// include/linux/string.h
...
/*
 * Include machine specific inline routines
 */
#include <asm/string.h>
...
#ifndef __HAVE_ARCH_MEMCPY
extern void * memcpy(void *,const void *,__kernel_size_t);
#endif
...

libc 的方案

libc-memcpy

A variation of the modified-GNU algorithm uses computation to adjust for address misalignment. I’ll call this algorithm the optimized algorithm . The optimized algorithm attempts to access memory efficiently, using 4-byte or larger reads-writes. It operates on the data internally to get the right bytes into the appropriate places. Figure 1 shows a typical step in this algorithm: memory is fetched on naturally aligned boundaries from the source of the block, the appropriate bytes are combined, then written out to the destination’s natural alignment.

copy_{to,from}_user()

  1. 为什么需要 copy_{to,from}_user(),它究竟在背后为我们做了什么?
  2. copy_{to,from}_user()和 memcpy() 的区别是什么,直接使用 memcpy() 可以吗?
  3. memcpy() 替代 copy_{to,from}_user() 是不是一定会有问题?

无论是内核态还是用户态访问合法的用户空间地址,当虚拟地址并未建立物理地址的映射关系的时候,page fault 的流程几乎一样,都会帮助我们申请物理内存并创建映射关系。所以这种情况下 memcpy()copy_{to,from}_user() 是类似的。

当内核态访问非法用户空间地址的时候,通过 .fixup__ex_table 两个段的帮助尝试修复异常。这种修复异常并不是建立地址映射关系,而是修改 do_page_fault() 返回地址。

memcpy() 由于没有创建这样的段,所以 memcpy() 无法做到这点。

禁止内核访问用户空间

在使能 CONFIG_ARM64_SW_TTBR0_PAN 或者 CONFIG_ARM64_PAN(硬件支持的情况下才有效)的时候,我们只能使用copy_{to,from}_user() 这种接口,直接使用 memcpy() 是不行的。两个配置选项的功能都是阻止内核态直接访问用户地址空间。只不过,CONFIG_ARM64_SW_TTBR0_PAN 是软件仿真实现这种功能,而 CONFIG_ARM64_PAN 是硬件实现功能(ARMv8.1扩展功能)。

config ARM64_SW_TTBR0_PAN
        bool "Emulate Privileged Access Never using TTBR0_EL1 switching"
        help
          Enabling this option prevents the kernel from accessing
          user-space memory directly by pointing TTBR0_EL1 to a reserved
          zeroed area and reserved ASID. The user access routines
          restore the valid TTBR0_EL1 temporarily. 

Arm64 使用两个页表基地址寄存器 ttbr0_el1ttbr1_el1
处理器根据 64 bit地址的高16 bit判断访问的地址属于用户空间还是内核空间。
如果是用户空间地址则使用 ttbr0_el1
如果是内核空间地址则使用 ttbr1_el1
如果我们希望禁止内核访问用户态地址,可以在进入内核后人为改变 ttbr0_el1,使其指向非法的映射即可。Linux 中为此准备了一份特殊的页表,这个页表大小为一个页,值全为0(映射非法)。因此只要将 ttbr0_el1 指向这个特殊的页表即可触发异常,使得内核无法访问用户态地址。

#define RESERVED_TTBR0_SIZE	(PAGE_SIZE)
 
SECTIONS
{
	reserved_ttbr0 = .;
	. += RESERVED_TTBR0_SIZE;
	swapper_pg_dir = .;
	. += SWAPPER_DIR_SIZE;
	swapper_pg_end = .;
}

当我们进入内核态后会通过 __uaccess_ttbr0_disable 切换 ttbr0_el1 以关闭用户空间地址访问,在需要访问的时候通过__uaccess_ttbr0_enable 打开用户空间地址访问。以 __uaccess_ttbr0_disable 为例说明:

.macro	__uaccess_ttbr0_disable, tmp1
    mrs	\tmp1, ttbr1_el1                        // swapper_pg_dir (1)
    bic	\tmp1, \tmp1, #TTBR_ASID_MASK
    sub	\tmp1, \tmp1, #RESERVED_TTBR0_SIZE      // reserved_ttbr0 just before swapper_pg_dir (2)
    msr	ttbr0_el1, \tmp1                        // set reserved TTBR0_EL1 (3)
    isb
    add	\tmp1, \tmp1, #RESERVED_TTBR0_SIZE
    msr	ttbr1_el1, \tmp1                       // set reserved ASID
    isb
.endm

在配置 CONFIG_ARM64_SW_TTBR0_PAN 的情况下,copy_{to,from}_user() 接口会在 copy 之前允许内核态访问用户空间,并在copy结束之后关闭内核态访问用户空间的能力。

gcc common attribute

https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes

__attribute__((weak))

大家日常工作中也许遇到过符号重复定义的错误。于是了解到程序中的符号定义分为强符号(Strong Symbol)和弱符号(Weak Symbol)。对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。强符号和弱符号都是针对定义来说的,不是针对符号的引用。比如 extern int a,表示 a 是一个外部变量的引用,在该文件内没有强弱符号的分别,其强弱由定义它的文件和下面的规则确定。

针对强弱符号的概念,链接器就会按如下规则处理与选择被多次定义的全局符号:
规则1:不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报符号重复定义错误。
规则2:如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。比如目标文件A定义全局变量global为int型,占4个字节;目标文件B定义global为double型,占8个字节,那么目标文件A和B链接后,符号global占8个字节(尽量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误)。

__attribute__((weakref))

目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确解析,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为 强引用(Strong Reference)。与之相对应还有一种 弱引用 (Weak Reference),在处理弱引用时,如果该符号有定义,则链接器将解析该符号引用;如果该符号未被定义,则链接器对于该引用不报错。链接器对于未定义的弱引用,不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。虽然链接阶段不会报错,但是如果该符号真的不存在,且程序中未做相应的处理则运行时会出错。

这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;如果去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。

weakref (“target”)
The weakref attribute marks a declaration as a weak reference.
Without arguments, it should be accompanied by an alias attribute naming the target symbol. Optionally, the target may be given as an argument to weakref itself. In either case, weakref implicitly marks the declaration as weak. Without a target, given as an argument to weakref or to alias, weakref is equivalent to weak.

static int x() __attribute__ ((weakref ("y")));
/* is equivalent to... */
static int x() __attribute__ ((weak, weakref, alias ("y")));
/* and to... */
static int x() __attribute__ ((weakref));
static int x() __attribute__ ((alias ("y")));

A weak reference is an alias that does not by itself require a definition to be given for the target symbol. If the target symbol is only referenced through weak references, then the becomes a weak undefined symbol. If it is directly referenced, however, then such strong references prevail, and a definition will be required for the symbol, not necessarily in the same translation unit.
The effect is equivalent to moving all references to the alias to a separate translation unit, renaming the alias to the aliased symbol, declaring it as weak, compiling the two separate translation units and performing a reloadable link on them.
At present, a declaration to which weakref is attached can only be static.

__attribute__((alias(“target”)))

The alias attribute causes the declaration to be emitted as an alias for another symbol, which must be specified.


void __f () { /* Do something. */; }
void f () __attribute__ ((weak, alias ("__f")));

给目标 __f 取了弱符号别名为 f__f 必须在同一个编译单元,否则会报错。

访存指令

值得注意的是寻址时的 post-indexed addressingpre-indexed addressing,以及地址的 writeback
以ARM为例:

LDRD<c> <Rt>, <Rt2>, [<Rn>{, #+/-<imm>}]
LDRD<c> <Rt>, <Rt2>, [<Rn>], #+/-<imm>
LDRD<c> <Rt>, <Rt2>, [<Rn>, #+/-<imm>]!

ldrd r0, r1, [r2, #4] // pre-index, no writeback, r2 不变
ldrd r0, r1, [r2, #4]! // pre-index, writeback, 最后 r2 = r2 + 4
ldrd r0, r1, [r2], #4 // post-index, writeback, 最后 r2 = r2 + 4
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章