译:Self-Modifying cod 和cacheflush

date: 2014-11-26 09:53

翻译自: 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可以同时进行取指令和取数据的操作。

使用 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指令的用法如下:

指令作用
MOVW把16 位立即数放到寄存器的低16 位,高16位清0
MOVT把16 位立即数放到寄存器的高16 位,低16位不影响

上图中,I-Cache一开始就装载了旧版指令。这并不总是正确,如果指令不曾执行那它存在I-cache中的可能性比较低,但不排除这种可能,比如指令预取。为了方便讨论,我们假定I-cache已经装载旧版指令。

处理器只能从I-cache中执行指令,同时只能从D-cache中“看到”数据(内存存储器对它就是透明的),通常处理器不能直接访问内存。对我们而言,我们需要记住:处理器不能直接执行存在于D-cache的“指令”并且不能被安排来读写I-cache中的“数据”。因为CPU不能直接往I-Cache(或内存)中写(指令),因此,当我们改写指令后,CPU看到的指令和数据是这样的:

如果现在尝试去执行修改后的代码,处理器将会忽略它而简单的执行旧的版本,因为对处理器来说,(旧版本)代码仍然在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)

转载于:https://my.oschina.net/u/3857782/blog/1857567

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值