date: 2014-11-26 09:53java
翻譯自: http://community.arm.com/groups/processors/blog/2010/02/17/caches-and-self-modifying-code數據結構
Cache處在CPU核心與內存存儲器之間,它給咱們的感受是,它具備「使之運行得更快」的魔力。固然,不一樣體系結構,其Cache也是千差萬別。在編寫代碼時,常見的建議是,大腦中有一個通用的Cache的概念就能夠了,這使得咱們能編寫出高效率的代碼。好比內核代碼中,某些數據結構其成員位置的「精心安排」,使得同時會被訪問的成員儘可能按cache line對齊。但在某些狀況,爲了保證咱們想要的結果,咱們必須考慮到cache的具體實現細節,自修改(Self-Modifying)代碼就是一種典型的狀況。架構
ARM架構有相互獨立的數據cache和指令cache,分別稱之爲D-cache和I-cache。正因如此,ARM架構常常被當作Modified Harvard Architecture(意即有各自獨立的數據總線和指令總線,能夠在這兩條總線上同時進行存取。與之相對的是von Neumann architecture,這種架構只有一條總線,不管是數據傳輸仍是指令傳輸都要走這條總線,所以取指令和讀(寫)數據不能同時進行)。Modified Harvard Architecture架構有不少優勢,爲了便於後面的討論,這裏只強調一點:由於有兩條總線的存在,CPU能夠同時進行取指令和取數據的操做。app
使用 Harvard-style memory interface自有它的優勢,好比效率提高;但它也有本身的缺點。對純 Harvard架構來講,一個典型的問題是:內存中的指令區(好比代碼段)不能被當作數據來直接訪問(這句話翻譯的可能有問題,不過不影響後面的討論。原話是:The typical drawback of a pure Harvard architecture is that instruction memory is not directly accessible from the same address space as data memory)。不過這種限制並無實施到ARM架構上。在ARM架構下,你能夠改寫指令(好比當前指令以後的某條指令)並將新的指令(指令實際上是一種特殊的數據)寫到內存中,可是由於D-cache和I-cache不一樣步,新寫的指令會被標記成「已經在I-cache中存在了(而再也不從內存中讀取)」,致使CPU最終執行的仍是老的指令。(這段話比較難懂吧,看可問題描述你就明白了)。函數
假定有這樣一段「自修改代碼」:其中包含及時編譯器(JIT)在運行時要動態生成本地指令的「字節碼」(不必定是java的字節碼),該「字節碼」要執行的操做是,將目標函數的地址加載到某個寄存中而後跳轉過去。及時編譯器(JIT compiler)已經將目標函數移到別處,所以須要更新指向它的指針(所以要修改「加載目標函數地址到寄存器」的指令)。這對及時編譯器來講,是再日常不過的操做了,一來目標函數的地址在編譯時不肯定,二來爲了對目標函數實施某些優化而可能將其重編譯至別處。 在修改指令以前,CPU看到的指令和數據是這樣的:優化
譯者注:movw和movt指令的用法以下:ui
指令 | 做用 |
---|---|
MOVW | 把16 位當即數放到寄存器的低16 位,高16位清0 |
MOVT | 把16 位當即數放到寄存器的高16 位,低16位不影響 |
上圖中,I-Cache一開始就裝載了舊版指令。這並不老是正確,若是指令未曾執行那它存在I-cache中的可能性比較低,但不排除這種可能,好比指令預取。爲了方便討論,咱們假定I-cache已經裝載舊版指令。this
處理器只能從I-cache中執行指令,同時只能從D-cache中「看到」數據(內存存儲器對它就是透明的),一般處理器不能直接訪問內存。對咱們而言,咱們須要記住:處理器不能直接執行存在於D-cache的「指令」而且不能被安排來讀寫I-cache中的「數據」。由於CPU不能直接往I-Cache(或內存)中寫(指令),所以,當咱們改寫指令後,CPU看到的指令和數據是這樣的:spa
若是如今嘗試去執行修改後的代碼,處理器將會忽略它而簡單的執行舊的版本,由於對處理器來講,(舊版本)代碼仍然在I-cache中而且CPU不知道代碼已經作了改動(沒人通知CPU說I-cache已經失效)。這對使用自修改代碼的Applications (such as JIT compilers)來講,的確是件討厭的事。操作系統
很明顯,咱們須要將數據(實際上是指令)從D-cache中「轉移」到I-Cache中。從上圖咱們知道,這隻有一條路:將D-Cache中數據寫到內存中,而後從內存中將指令裝載到I-Cache中。 在未來的某個時間點,CPU可能會將D-cache中的數據寫到內存中,並從內存中重寫裝載指令到I-Cache中,但具體在什麼時候咱們不得而知,所以沒法將但願寄託在CPU不肯定的行爲身上,咱們要馬上、如今就解決它。如今,D-cache中的數據爲新的,與內存中的內容已經不一致了,於是是髒數據。毫無疑問,爲了將數據寫到內存中,咱們只需clean它,並等待回寫完成。此時,結果以下:
爲了執行修改後的代碼,咱們須要通知處理器,I-cache中的指令已經「過期」,須要從內存中重現裝載。咱們經過使I-cache失效(invalidating)來達到此目的。此時結果以下:
如今,若是咱們再去嘗試執行修改後的指令,取指操做將遭遇I-cache miss(未命中),因而就從內存中從新裝載,正如咱們所料,此次執行的將是修改後的代碼。 然而,這並非事實的所有,還有一些其餘的事情須要咱們去作。若是處理器自帶分支預測(branch prediction),咱們還得清除跳轉目標緩衝器(branch target buffer,BTB)。一般,處理器會將寫內存的操做放在一個緩衝隊列中緩衝起來。因此在清(clean)D-cache前,必須完成這些寫內存的操做。固然,這些操做是與具體處理器架構相關的。你也能夠用一個庫函數來幹這些「雜事」。若是你只是爲了寫自修改代碼,那麼理解你的庫函數都幹了些啥以及爲啥要這樣幹就能夠了。至於具體CPU架構的底層細節,就無需關注了。
最後,你可能想過利用PLI指令來給處理器一個提示,讓他從新裝載指令到I-Cache中。這可能會給你帶來可觀的效率提高, as it will not have to stall on memory when you eventually branch to it(這句不懂)。固然,既然是提示,處理器可能會忽視它而不起做用,但在某些實現上它仍是有益的。
譯者注:PLI 預取指令,這是服務於cache 系統的一條 hint 指令。
一般,執行這些任務的相關指令爲CP15 (System Control Coprocessor) 操做,不能在非特權模式下執行。這意味着必須藉助操做系統(內核)來完成這些操做(系統調用陷入內核後,CPU即處在特權模式)。
在linxu系統中,若是用gcc編譯,能夠調用 __clear_cache()函數,而在Windwos CE系統中能夠調用FlushInstructionCache()函數。
對Android操做系統來講,libc庫提供了cacheflush()函數,咱們來看看該函數的實現(這部分爲譯者添加,若是不想了解細節能夠跳過)。
原型爲:
/* A special syscall that is only available on the ARM, not x86 function. */ int cacheflush(long start, long end, long flags);
其對應的實如今cacheflush.s中
ENTRY(cacheflush) .save {r4, r7} stmfd sp!, {r4, r7} ldr r7, =__NR_ARM_cacheflush swi #0 ldmfd sp!, {r4, r7} movs r0, r0 bxpl lr b __set_syscall_errno END(cacheflush)
cacheflush經過swi #0陷入內核,其系統調用號爲__NR_ARM_cacheflush。
在內核端,__NR_ARM_cacheflush的定義在<kernel/arch/arm/include/asm/unistd.h>中:
#define __NR_SYSCALL_BASE 0 /* * The following SWIs are ARM private. */ #define __ARM_NR_BASE (__NR_SYSCALL_BASE+0x0f0000) #define __ARM_NR_cacheflush (__ARM_NR_BASE+2)
可見系統調用號__ARM_NR_cacheflush爲0x0f0002。
再來看內核的實現(定義在<kernel/arch/arm/kernel/traps.c>文件中):
#define NR(x) ((__ARM_NR_##x) - __ARM_NR_BASE) asmlinkage int arm_syscall(int no, struct pt_regs *regs) { ... /* * Flush a region from virtual address 'r0' to virtual address 'r1' * _exclusive_. There is no alignment requirement on either address; * user space does not need to know the hardware cache layout. * * r2 contains flags. It should ALWAYS be passed as ZERO until it * is defined to be something else. For now we ignore it, but may * the fires of hell burn in your belly if you break this rule. ;) * * (at a later date, we may want to allow this call to not flush * various aspects of the cache. Passing '0' will guarantee that * everything necessary gets flushed to maintain consistency in * the specified region). */ case NR(cacheflush): do_cache_op(regs->ARM_r0, regs->ARM_r1, regs->ARM_r2); return 0; ... }
可見,最終調用do_cache_op(),該函數的實現也在本文件中:
static inline void do_cache_op(unsigned long start, unsigned long end, int flags) { struct mm_struct *mm = current->active_mm; struct vm_area_struct *vma; if (end < start || flags) return; down_read(&mm->mmap_sem); vma = find_vma(mm, start); if (vma && vma->vm_start < end) { if (start < vma->vm_start) start = vma->vm_start; if (end > vma->vm_end) end = vma->vm_end; up_read(&mm->mmap_sem); flush_cache_user_range(start, end); return; } up_read(&mm->mmap_sem);
vma便是給定地址區間[start, end)(前閉後開區間)對應的虛存區間,內核用vm_area_struct 結構來管理虛存空間,cacheflush()傳進來的地址區間必須是有效的。進行必要的檢查後,do_cache_op()調用 flush_cache_user_range() 執行核心操做。
flush_cache_user_range 是一個宏,其定義在<kernel/arch/arm/include/asm/cacheflush.h>:
/* * flush_cache_user_range is used when we want to ensure that the * Harvard caches are synchronised for the user space address range. * This is used for the ARM private sys_cacheflush system call. */ #define flush_cache_user_range(start,end) \ __cpuc_coherent_user_range((start) & PAGE_MASK, PAGE_ALIGN(end))
__cpuc_coherent_user_range()是一個與CPU相關的函數,對ARMv7來講,其定義在<kernel/arch/arm/mm/cache-v7.s>中,要讀懂這些代碼須要瞭解ARM的技術手冊。這裏咱們只關注'@'符號引導的註釋,正如前文所說,這裏幹了三件事:
代碼以下:
/* * v7_coherent_user_range(start,end) * * Ensure that the I and D caches are coherent within specified * region. This is typically used when code has been written to * a memory region, and will be executed. * * - start - virtual start address of region * - end - virtual end address of region * * It is assumed that: * - the Icache does not read data from the write buffer */ ENTRY(v7_coherent_user_range) UNWIND(.fnstart ) dcache_line_size r2, r3 sub r3, r2, #1 bic r12, r0, r3 #ifdef CONFIG_ARM_ERRATA_764369 ALT_SMP(W(dsb)) ALT_UP(W(nop)) #endif 1: USER( mcr p15, 0, r12, c7, c11, 1 ) @ clean D line to the point of unification add r12, r12, r2 cmp r12, r1 blo 1b dsb icache_line_size r2, r3 sub r3, r2, #1 bic r12, r0, r3 2: USER( mcr p15, 0, r12, c7, c5, 1 ) @ invalidate I line add r12, r12, r2 cmp r12, r1 blo 2b 3: mov r0, #0 ALT_SMP(mcr p15, 0, r0, c7, c1, 6) @ invalidate BTB Inner Shareable ALT_UP(mcr p15, 0, r0, c7, c5, 6) @ invalidate BTB dsb isb mov pc, lr /* * Fault handling for the cache operation above. If the virtual address in r0 * isn't mapped, just try the next page. */ 9001: mov r12, r12, lsr #12 mov r12, r12, lsl #12 add r12, r12, #4096 b 3b UNWIND(.fnend ) ENDPROC(v7_coherent_user_range)