譯:Self-Modifying cod 和cacheflush

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最終執行的仍是老的指令。(這段話比較難懂吧,看可問題描述你就明白了)。函數

1.問題描述

假定有這樣一段「自修改代碼」:其中包含及時編譯器(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)來講,的確是件討厭的事。操作系統

2.問題解決

很明顯,咱們須要將數據(實際上是指令)從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 指令。

3.代碼

一般,執行這些任務的相關指令爲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的技術手冊。這裏咱們只關注'@'符號引導的註釋,正如前文所說,這裏幹了三件事:

  • clean D-cache
  • invalidate I-cache
  • invalidate BTB

代碼以下:

/*
     * 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)
相關文章
相關標籤/搜索