文档章节

译:Self-Modifying cod 和cacheflush

我叫半桶水
 我叫半桶水
发布于 07/16 19:51
字数 2826
阅读 21
收藏 0

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)

© 著作权归作者所有

共有 人打赏支持
我叫半桶水
粉丝 0
博文 26
码字总数 71642
作品 0
西安
私信 提问
二进制小分队主攻计算机国外视频翻译,语言和技术都有兴趣的请戳我们!

Hey! 大家好! 二进制小分队也许是国内第一个专注于译制软件开发领域专业教程和资讯视频的字幕组。字幕组成立已有近半年时间,当前成员10人,保持活跃,产出稳定。我们认为专业的内容应该由专...

二进制小分队
2017/04/08
155
1
【译】使用 Python 编写虚拟机解释器

【译】如何使用 Python 创建一个虚拟机解释器? 原文地址:Making a simple VM interpreter in Python 更新:根据大家的评论我对代码做了轻微的改动。感谢 robin-gvx、 bs4h 和 Dagur,具体代...

OneAPM蓝海讯通
2015/06/19
298
5
记一次valgrind引发的打桩失败问题的定位

Valgrind是Linux下用来检查程序是否有内存泄漏的利器。现在每次运行完UT之后,都会用valgrind跑一下程序,看看有没有内存泄漏的问题。如果你的程序从来没有用valgrind跑过,也没有在代码中置...

阿涵_Jiang
06/16
0
0
是的,Safari 支持 Service Worker 了

12月 20日,Apple 发布的 Safari 46 技术预览版里,Mac 端的 Safari 将默认打开 Service Worker, 是的,Safari 支持 Service Worker 了,PWA 时代不远了。 其实回顾一下, 2017 年 7 月 14...

2017/12/21
0
0
sql injection violation问题

@wenshao 你好,想跟你请教个问题: 在使用jFinal开发的系统,使用jetty嵌入式开发中不存在此问题,当部署到tomcat或jetty服务器上就出现此问题,使用C3P0也不存在此问题。出错的SQL如下: ...

孤竹行
2014/06/05
238
1

没有更多内容

加载失败,请刷新页面

加载更多

PHP生成CSV之内部换行

当我们使用PHP将采集到的文件内容保存到csv文件时,往往需要将采集内容进行二次过滤处理才能得到需要的内容。比如网页中的换行符,空格符等等。 对于空格等处理起来都比较简单,这里我们单独...

豆花饭烧土豆
今天
2
0
使用 mjml 生成 thymeleaf 邮件框架模板

发邮件算是系统开发的一个基本需求了,不过搞邮件模板实在是件恶心事,估计搞过的同仁都有体会。 得支持多种客户端 支持响应式 疼彻心扉的 outlook 多数客户端只支持 inline 形式的 css 布局...

郁也风
今天
8
0
让哲学照亮我们的人生——读《医务工作者需要学点哲学》有感2600字

让哲学照亮我们的人生——读《医务工作者需要学点哲学》有感2600字: 作者:孙冬梅;以前读韩国前总统朴槿惠的著作《绝望锻炼了我》时,里面有一句话令我印象深刻,她说“在我最困难的时期,...

原创小博客
今天
4
0
JAVA-四元数类

public class Quaternion { private final double x0, x1, x2, x3; // 四元数构造函数 public Quaternion(double x0, double x1, double x2, double x3) { this.x0 = ......

Pulsar-V
今天
18
0
Xshell利用Xftp传输文件,使用pure-ftpd搭建ftp服务

Xftp传输文件 如果已经通过Xshell登录到服务器,此时可以使用快捷键ctrl+alt+f 打开Xftp并展示Xshell当前的目录,之后直接拖拽传输文件即可。 pure-ftpd搭建ftp服务 pure-ftpd要比vsftp简单,...

野雪球
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部