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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章