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)