Blogs

jaywalking在编译器周围

杰森萨赫斯2019年12月9日3评论

我们的团队有另一个 代码评论 最近。当我看到一个看起来像这样的函数时,我看了一个文件,直立地猛烈地猛烈地猛烈地猛烈地刺手:

void some_function(SOMEDATA_T *psomedata)
{
    asm volatile("push CORCON");
    CORCON = 0x00E2; 

    do_some_other_stuff(psomedata);

    asm volatile("pop  CORCON");
}

那里 is a serious bug here —你看到它是什么吗?

把甲板堆叠在一起

好的,首先,这个功能在做什么?有关的C代码是写入Microchip DSPIC33E设备的。这 CORCON 在DSPIC33E设备中注册 is the “核心控制寄存器” —它控制了处理器所做的一堆杂项。它’s a set of 模式位. Some of the bits control DSP instructions; the E2 content essentially tells the compiler to turn on arithmetic saturation and rounding, and use fractional multiplication, when working with the DSP accumulators.

因此代码的意图大致如下:

  • save the CORCON register on the stack
  • configure CORCON to turn on saturation and rounding, and use fractional multiplication for certain DSP instructions
  • 执行一些计算
  • restore the CORCON register from the stack

但它赢了’T按预期工作。这里有几个错误。

One is that by setting CORCON = 0x00E2, 全部 the bits of CORCON are being set a specific way, including some which affect the system interrupt priority level and how DO loops behave.

What the code should have done is set or clear only the relevant bits of CORCON, and leave the other bits alone.

The more serious bug has to do with the way that CORCON is saved and restored, and the way that the compiler interacts with inline assembly code.

你看,当你在C中使用内联组装时,你必须非常小心’有点像表演心脏手术…虽然病人仍然醒着和走路… and the patient isn’意识到外科医生。好吧,我希望这令你害怕你,但真的没有’T非常好的解释危险。所以在这里:要注意的重要事项是CPU和内存的某些部分,如堆栈,是 管理 由编译器,并且有些事情是编译器 认为s。这些字 管理认为s 很重要,就像我一样’m写这一点,他们触发了一些关联给我。

  • 管理 — You don’t触摸堆栈。编译器拥有堆栈。堆栈不是您可以直接读取或写入的东西。放手。如果你用堆栈播放,编译器会伤害你。请留下堆栈。不要依赖于读取堆栈的内容。请不要’与堆栈的猴子。它’由编译器达到使用堆栈。单独离开堆栈。非常非常害怕操纵堆栈的C代码。大学教师’甚至想想它。危险,禁止入内。

(实际上, 管理 让我想起 托管代码 从2005-2008时代左右,即它是一些奇怪的Microsoft功能的C ++或.NET,所以没有意义,让我只是想留下来。微软会为我管理一些东西,并保护我免受自己,好的,这是什么,一堆方面的方块,为什么我想用这个?)

堆栈是由编译器生成的输出程序的实现细节,为编译器保留的内存部分为a Lifo数据结构 有一件叫做的东西 堆栈指针 which keeps track of the top of the stack. As the compiler generates code to save temporary data, it puts it on the stack and moves the stack pointer, until later when it no longer needs that data and it restores the stack pointer back. Strictly speaking, the compiler could use a completely different mechanism than the CPU stack, and often it optimizes things and places local variables in registers. If you write code like the excerpt below, you have no idea where the variable thing may be placed:

int foo(int a)
{
    int thing = a*a;

    thing += do_some_stuff(a);
    return thing;
}

它可能在堆栈上。它可能在寄存器中。编译器可以动态分配内存(但可能赢了’t)或将其粘在内存中的其他一些预定义的地方(但几乎肯定赢了’t)。你没有办法知道,没有 看着编译器正在做什么。编译器必须表现出某种预定义的方式,我们’Re告诉它通常使用堆栈用于局部变量,但这’决定它的决定。

  • 认为s —该编译器不仅为您管理堆栈,还提供了它 认为s 这是堆栈的唯一所有者。它假设关于内联汇编代码的某些事情,即它没有任何责任检查该代码正在做的事情,并且您作为程序员,您负责确保您与编译器正确合作。从技术上讲,除非你用汇编代码正确界面(如 GCC扩展了ASM语法),你几乎不能做任何事情。编译器对待您作为黑匣子的写作,并假设您的荣誉’触摸你应该的任何东西’T。其中包括堆栈。 (我提到的是你应该’t modify the stack?)

这意味着什么是处理器和编译器依赖;让’查看XC16 C编译器用户’引导指南,其中包含以下内容 第12.3节 — with my emphasis on 管理认为s:

12.3更改寄存器内容

编译器由C源代码生成的组件将使用某些寄存器 存在于16位设备上。最重要的是,编译器 认为s 它生成的代码中没有任何内容可以更改这些寄存器的内容。所以,如果是 装配用值加载寄存器,并且不需要后续代码生成 该寄存器,编译器将假设寄存器的内容稍后仍然有效 在输出序列中。

是特殊的寄存器和哪些寄存器 管理 由编译器是:W0-W15, RCOUNT,状态(SR),PSVPAG和DSRPAG。如果启用了固定点支持,则 编译器可以分配A和B,在这种情况下,编译器可以调整CORCON。

这些寄存器的状态永远不会通过C代码直接更改,或者由任何汇编代码与C代码一致。以下示例显示了C语句和在线 违反这些规则的程序集并更改状态寄存器中的零位。

#include <xc.h>
void badCode(void)
{
 asm (“mov #0, w8”);
 WREG9 = 0;
}

编译器无法解释C代码中遇到的在线装配代码的含义。它也没有将映射到SFR映射到实际的变量 注册自己。使用这两种方法中的任何一个写入SFR寄存器不会标记 寄存器已更改,可能导致代码失败。

这听起来很好,花花公子,但它的意思是什么?

三个错误造成灾难

让’S看一个自主实体假设某些东西的情况。您可能已经听说过2018年3月18日,亚利桑那州坦佩的死亡,涉及由Uber Technologies,Inc。和行人拥有的自驾驶车辆。你听“pedestrian” and “self-driving vehicle”也许它可能让人愿意在一个繁忙的街市街道上行走的快乐幸运的家伙,只是在一个接近的自驾车决定撞到他身边之前。好吧,这不是那样的。这里’s 最近来自华盛顿邮政的文章 关于2018年3月的意外,我强调“assume”:

但是,国家运输安全委员会周二发布的文件显示优步’S自驱动系统使用缺陷编程 假设 关于一些道路用户可能表现的。尽管在击中49岁的Elaine Herzberg之前,尽管有足够的时间停止— nearly 6 seconds —这些系统反复未能准确地将她作为行人分类或者了解她正在将她的自行车推在坦佩,阿里兹的交通车道上。,街道在10点之前不久。

优步’S自动化驾驶系统“从不将她作为行人分类—或预测她的目标是jaywalking行人或骑自行车的人” —因为她在一个没有人行横道的地区穿过,所以发现了NTSB调查人员。“系统设计不包括对偶数行人的考虑因素。”

系统摇摇欲坠,是否将赫尔兹伯格作为车辆,自行车或自行车“an other,” so “系统无法正确预测检测到的对象的路径,”根据NTSB的调查报告,该报告设定为本月晚些时候会面,以发出其导致赫尔兹伯格的决定’s death.

在一个特别有问题的优步 假设,鉴于公共道路上常见的混乱,系统 认为d 将对象分类为“other”将留在那里“static location.”

也就是说,自主车辆有一定的规则:假设检测到的物体落入一组固定的可预见行为。

假定?

自动车辆和编译器aren’真的能够假设任何东西。那’没有什么是确定性系统所做的事情,无论我们有多少钱,我们都会拟人。它们包含前提条件和断言。相反,他们的系统设计师是这样做的假设。 (我打算说自动车辆假设没有像jaywalking行人那样的东西,或者它假设没有行人会穿过一条繁忙的街道,但这些陈述意味着设计师知道这种可能性并编程他们的系统包括这些概念,但拒绝使用它们。更有可能是他们在被编程到系统中的潜在行为之外。)

对于记录, 2018年3月的NTSB报告崩溃但值得注意的是 汽车自动化报告, 这 公路集团事实报告, 这 人类绩效集团主席’s Factual Report,而且 在床上映像&数据记录器集团董事长’s Factual Report,没有使用这个词“assume”当参考自动化驱动系统(广告)时:

…但是,某些对象分类—其他—没有分配目标。对于这些对象,将其目前检测到的位置被视为a 静态;除非该位置直接在自动化车辆的路径上,否则该对象是 不被视为可能的障碍....

在广告首次检测到行人时,5.6秒前 冲击,她的位置大约在两个左转通道中间(见图3)。 虽然广告在撞击前近6秒感测了行人,但系统从不 将她分类为行人—或者预测到她的目标是jaywalking行人或一个 cyclist—因为她在没有人行横道的地方穿过N. Mill Avenue;系统 设计不包括对jaywalking行人的考虑因素。相反,系统最初 classified her as an 其他 未分配目标的对象。随着广告改变了 分类了几次行人—车辆,自行车和另一个之间交替— 系统无法正确预测检测到的对象的路径。

另一方面,我使用这个词没有问题“assume” —不是因为我想暗示任何类型的中兴,或者在技术上是完全准确的,而是因为我们可以将其作为基于有限的数据集和简化的规则集得出结论的速记。

无论如何,这次事故是一个悲剧,有许多贡献因素—一条城市工业区的街道在高架高速公路附近,晚上,每小时45英里的速度限制;一名行人决定穿过这条街道,穿着深色衣服,并在一个没有为行人过境的地区散步和骑自行车;一个具有某些可疑假设的系统;一个人“operator”谁能采取行动停车,但谁在车内瞥了一眼 观看NBC的流媒体视频’s 声音。与我的主题最相关的假设是那个最相关的

  • 行人假设它足以在没有人行横道的位置交叉—据推测,如果一辆汽车做了方法,它的司机会看到她并避免碰撞。
  • 车辆假设检测到的物体不是一条穿越道路的行人,那个物体’S轨迹不是一个导致碰撞的轨迹。
  • 操作员假设观看流媒体视频并依靠车辆来驱动自身。

所有三个假设都是错误的,发生了碰撞。


亚利桑那修订了法规,标题28 - 交通

28-793。穿越除了人行横道

  1. 在交叉口在标记的人行横道中或非标记的人行横道内的任何一点的行人横穿巷道,应给道路上的所有车辆产生正确的方式。
  2. 在提供行人隧道或高架行人交叉的一点横穿道路的行人应为道路上的所有车辆产生正确的方式。
  3. 在运行流量控制信号的相邻交叉点之间,行人不得在任何地方交叉,除了标记的人行横道。

28-794。司机行使适当的护理

尽管本章的规定,车辆的每个驾驶员都应:

  1. 锻炼锻炼,避免与任何道路上的任何行人碰撞。

  2. 必要时发出喇叭发出警告。

  3. 在途中观察儿童或困惑或无能为力的人的行使适当的预防措施。

装配地上的碰撞

后退 to our CORCON example. The PUSHPOP instructions cause the stack pointer W15 to be modified and data written to and read from the stack. But the compiler 管理 the stack pointer W15, and uses it however it sees fit, 假设 no one else is going to modify W15 or the memory it points to. It also uses register W14 as a frame pointer using the LNKULNK instructions. (If you want more of the technical details, look at the 程序员’s Reference Manual 。)

让’看看一个简单的例子。一世’m using XC16 1.41:

import pyxc16

for optlevel in [1,0]:
    print "// --- -O%d ---" % optlevel
    pyxc16.compile('''
    #include <stdint.h>

    int16_t add(int16_t a, int16_t b)
    {
        return a+b;
    }
    ''', '-c','-O%d' % optlevel)
// --- -O1 ---
_add:
	add	w1,w0,w0
	return
// --- -O0 ---
_add:
	lnk	#4
	mov	w0,[w14]
	mov	w1,[w14+2]
	mov	[w14+2],w0
	add	w0,[w14],w0
	ulnk
	return

编译器输出有点不同,具体取决于我们是否关闭或打开。

With optimization on (-O1) the compiler can reduce this function to a single ADD instruction, and the stack isn’t used at all except for the return address (CALL pushes the return address onto the stack; RETURN pops it off.)

With optimization off (-O0), the compiler takes the following steps:

  • it allocates a new stack frame of 4 bytes using the LNK instruction —这将旧帧指针W14推到堆栈上,将堆栈指针复制到W14中作为新帧指针,并在堆栈上分配4个附加字节
  • it copies the first argument a from W0 to its place in the stack frame [W14], and copies the second argument b from W1 to its place in the stack frame [W14+2]
  • it performs the required computation, taking its inputs from those places (a = [W14]b = [W14+2]) and puts the result into W0
  • 它与堆栈框架解析 ULNK

很多这项工作都是不必要的,但这’s what happens with -O0; you’LL看到一堆机械,可预测和安全的行为来实现C程序员请求的内容。

现在我们’re going to spice it up by adding some jaywalking to mess with CORCON in inline assembly.

for optlevel in [1,0]:
    print "// --- -O%d ---" % optlevel
    pyxc16.compile(r'''
    #include <stdint.h>

    extern volatile uint16_t CORCON;

    int16_t add(int16_t a, int16_t b)
    {
        asm volatile("\n_l1:\n        push CORCON\n_l2:");
        CORCON = 0x00e2;
        
        int16_t result = a+b;
        
        asm volatile("\n_l3:\n        pop CORCON\n_l4:");
        return result;
    }
    ''', '-c','-O%d' % optlevel)
// --- -O1 ---
_add:
_l1:
        push CORCON
_l2:
	mov	#226,w2
	mov	w2,_CORCON
_l3:
        pop CORCON
_l4:
	add	w1,w0,w0
	return
// --- -O0 ---
_add:
	lnk	#6
	mov	w0,[w14+2]
	mov	w1,[w14+4]
_l1:
        push CORCON
_l2:
	mov	#226,w0
	mov	w0,_CORCON
	mov	[w14+2],w1
	mov	[w14+4],w0
	add	w1,w0,[w14]
_l3:
        pop CORCON
_l4:
	mov	[w14],w0
	ulnk
	return

我在这’ve added labels _l1 through _l4 to help capture what’在某些瞬间进行。

Now, the -O1 case is fairly easy to understand; here we push CORCON onto the stack, write E2 = 226 into CORCON, pop CORCON back off the stack, then do our adding operation.

呵呵?

The C code asked the compiler to add a+b in between the PUSHPOP calls. So that’s another bug —不在编译器中,但在我们与之交互的方式。编译器 认为s 它可以重新排序某些事情;它没有’t know what you’re trying to do with this inline assembly, it just knows that you want to return a+b, and that’s what it does. We’ll look at that again in a bit. (Yes, I know that the ADD instruction isn’t affected by CORCON, but if we were using a DSP instruction like MPY or MAC, 这n this bug could produce incorrect behavior.)

The -O0 case is a little more involved, and it does do the math between labels _l2_l3 after setting CORCON to E2. Here’S堆栈在不同的时刻看起来像什么:

黄色细胞是我们的’通过内联组装手动添加;他们的其余部分已由编译器处理。“FP” stands for frame pointer (W14 in the dsPIC) and “SP” for stack pointer (W15); the “addr.lo” and “addr.hi” content is the return address which has been placed onto the stack when add() is reached via a CALL instruction.

The stack on the dsPIC grows upwards in memory with PUSHLNK instructions, and either contains allocated or unallocated data:

  • 分配堆栈指针下方的地址的内容—编译器(或我们,如果我们’愚蠢地尝试使用它)将仅在故意更改堆栈上分配的特定值时修改此内容,并且预期通过恢复堆栈完成使用它时会分配此内容指向其以前的位置。

  • 未分配堆栈指针或上方的地址的内容—允许编译器用于临时数据,可以分配和解析内存。我们可以’t假设我们知道未分配数据的内容,因为在任何即时都可能发生中断,并在堆栈上分配/使用/使用/已解除的内存。我标记了所有未知的内容???

So even though we just had the saved value of CORCON on the stack at _l3, we aren’t allowed to assume that this value will still be there at _l4.

否则,这里没有问题;编译器做到了它的事情,我们做了我们的事情,一切都很好,对吧?当汽车aren时,就像在街上跑过’t coming.

让’s raise it up another notch: below is a function foo() which is just like add()但它 calls some external function munge() to modify the result a+b before returning it. If you want to test it yourself, create a different file that contains something like

#include <stdint.h>

void munge(int16_t *px)
{
    (*px)++;   // add 1 to whatever px points to
}
for optlevel in [0,1]:
    print "// --- -O%d ---" % optlevel
    pyxc16.compile(r'''
    #include <stdint.h>

    extern volatile uint16_t CORCON;
    
    void munge(int16_t *px);
    
    int16_t foo(int16_t a, int16_t b)
    {
        asm volatile("\n_l1:\n        push CORCON\n_l2:");
        CORCON = 0x00e2;
        
        int16_t result = a+b;
        asm volatile("\n_l3:");
        munge(&result);
        
        asm volatile("\n_l4:\n        pop CORCON\n_l5:");
        return result;
    }
    ''', '-c','-O%d' % optlevel)
// --- -O0 ---
_foo:
	lnk	#6
	mov	w0,[w14+2]
	mov	w1,[w14+4]
_l1:
        push CORCON
_l2:
	mov	#226,w0
	mov	w0,_CORCON
	mov	[w14+2],w1
	mov	[w14+4],w0
	add	w1,w0,w0
	mov	w0,[w14]
_l3:
	mov	w14,w0
	rcall	_munge
_l4:
        pop CORCON
_l5:
	mov	[w14],w0
	ulnk
	return
// --- -O1 ---
_foo:
	lnk	#2
_l1:
        push CORCON
_l2:
	mov	#226,w2
	mov	w2,_CORCON
	add	w1,w0,w1
	mov	w1,[w15-2]
_l3:
	dec2	w15,w0
	rcall	_munge
_l4:
        pop CORCON
_l5:
	mov	[w15-2],w0
	ulnk
	return

让’s look at the unoptimized -O0 version first.

This looks a lot like the add() case, except here we rcall _munge with the value of W14 as an argument by placing it in W0 — we’re passing in the address contained in the frame pointer. Then munge() can read and write this value as appropriate. After munge() completes:

  • pop off the saved value of CORCON和put it back into the CORCON register
  • copy the munged value of a+b into W0 as the return value
  • 解除堆栈框架并返回到呼叫者

再次,这里没有问题。

但在这儿’s what happens when this is compiled in -O1:

  • _foo_l1 —分配双字节堆栈帧以存储临时值
  • _l1_l2 — push the value of CORCON on the stack
  • _l2_l3 — write E2 into CORCON, compute a+b, and store it in the location below the stack pointer = [W15-2]. Uh oh. 这里’碰撞发生的地方。 We saved our CORCON value on the stack, but the compiler doesn’t know it’s there and thinks that [W15-2] is where it allocated the two bytes on the stack, which it owns, and which it can safely modify. If the compiler were aware that we allocated two more bytes on the stack using inline assembly, then it should be storing a+b at [W15-4]… but it’s not aware, and instead, the compiler-generated code overwrites the saved value of CORCON.
  • _l3_l4 – call munge(), passing in W15-2 as an argument by placing it in W0
  • _l4_l5 — our inline assembly is executed, and the CPU pops what we think is the saved value of CORCON back into the CORCON register. But instead, it’s the munged value of a+b.
  • _l5 → return from foo() — copy these two allocated bytes on the stack into W0 to use as a return value, then deallocate the two-byte stack frame. Unfortunately the compiler thinks those bytes contain the munged value of a+b, whereas in reality they contain uninitialized memory.

除非您在将控件返回到编译器之前,否则您不能使用内联汇编程序来分配堆栈内存。 That is, if you’re going to execute PUSH in a section of inline assembly, that same section has to contain a corresponding POP. Otherwise, your inline assembly conflicts with what the compiler assumes about what’s位于分配的堆栈的顶部,一旦发生这种情况,所有投注都已关闭;编译器管理可以由我们的手动混合使用内联组合程序损坏的指针和内存,结果可能会导致意外行为。 这不是良性失败!

如果您有MPLAB X和XC16编译器的副本,您可以尝试自己运行此操作,并且您使用模拟器调试。

If you step through the code, you will find the results of the collision after foo() returns:

  • Instead of restoring its original value, CORCON will contain the munged version of a+b in the bits of CORCON that are writeable (some bits are read-only)
  • The result of foo() will be whatever value happened to be at the appropriate place on the stack immediately below where CORCON gets PUSHed and the value of a+b gets storesd. (So if the stack pointer W15 contained 0x1006 before the call to foo(), 这n the “addr.lo” word in the diagram is located at 0x1006,而且 result of foo() will be whatever value is contained three words past it, at address 0x100c, whereas a+b will get stored at 0x100e, and munge() will modify the value located there.)

拯救corcon的正确方法

那么我们如何解决这个问题?好吧,那里’s still a way we can save CORCON without corrupting the compiler’■对系统状态的理解,这就是将其粘贴在局部变量中:

for optlevel in [0,1]:
    print "// --- -O%d ---" % optlevel
    pyxc16.compile(r'''
    #include <stdint.h>

    extern volatile uint16_t CORCON;
    
    void munge(int16_t *px);
    
    int16_t foo(int16_t a, int16_t b)
    {
        uint16_t tempCORCON = CORCON;
        CORCON = 0x00e2;
        
        int16_t result = a+b;
        munge(&result);
        
        CORCON = tempCORCON;
        return result;
    }
    ''', '-c','-O%d' % optlevel)
// --- -O0 ---
_foo:
	lnk	#8
	mov	w0,[w14+4]
	mov	w1,[w14+6]
	mov	_CORCON,w1
	mov	w1,[w14]
	mov	#226,w0
	mov	w0,_CORCON
	mov	[w14+4],w1
	mov	[w14+6],w0
	add	w1,w0,w0
	mov	w0,[w14+2]
	inc2	w14,w0
	rcall	_munge
	mov	[w14],w1
	mov	w1,_CORCON
	mov	[w14+2],w0
	ulnk
	return
// --- -O1 ---
_foo:
	lnk	#2
	mov	w8,[w15++]
	mov	_CORCON,w8
	mov	#226,w2
	mov	w2,_CORCON
	add	w1,w0,w1
	mov	w1,[w15-4]
	sub	w15,#4,w0
	rcall	_munge
	mov	w8,_CORCON
	mov	[w15-4],w0
	mov	[--w15],w8
	ulnk
	return

这里的编译器正在管理一切,它可以假设它在堆栈上分配的东西将在它预期的状态下留在那里,除非它修改分配的内存本身。

其他微妙之处

计算依赖性和执行顺序

那里’s still that other little bug we ran into in add() under -O1, namely that the addition happened outside of the section of code in which CORCON was saved and restored. This bug will still be there even if get rid of our use of inline assembly; see below, where the ADD instruction takes place after we’ve restored CORCON:

pyxc16.compile(r'''
#include <stdint.h>

extern volatile uint16_t CORCON;

int16_t add(int16_t a, int16_t b)
{
    uint16_t tempCORCON = CORCON;
    CORCON = 0x00e2;

    int16_t result = a+b;

    CORCON = tempCORCON;
    return result;
}
''', '-c','-O1')
_add:
	mov	_CORCON,w2
	mov	#226,w3
	mov	w3,_CORCON
	mov	w2,_CORCON
	add	w1,w0,w0
	return

The problem here is that the compiler has no knowledge of data dependency between the content of the CORCON register and the instruction we want to execute. Again — yes, the ADD instruction isn’t affected, but the same problem could occur if we use an accumulator instruction that depends on the CORCON content, like SAC.R:

pyxc16.compile(r'''
#include <stdint.h>

extern volatile uint16_t CORCON;
register int accA asm("A");    // accumulator A

int16_t bar(int16_t a, int16_t b)
{
    uint16_t tempCORCON = CORCON;
    CORCON = 0x00e2;

    accA = __builtin_lac(a+b, 3);
    int16_t result = __builtin_sacr(accA, 4);

    CORCON = tempCORCON;
    return result;
}
''', '-c','-O1')
_bar:
	mov	_CORCON,w2
	mov	#226,w3
	mov	w3,_CORCON
	add	w1,w0,w0
	lac	w0, #3, A
	sac.r	A, #4, w0
	mov	w2,_CORCON
	return

在这种情况下,它就没有’t, but it’我不清楚我是否可以依赖这个C代码来工作—换句话说,编译器是否知道它可以’t reorder an accumulator __builtin with respect to a volatile memory access.

We can force our add() function to not reorder by computing the result in a volatile local variable.

pyxc16.compile(r'''
#include <stdint.h>

extern volatile uint16_t CORCON;

int16_t add(int16_t a, int16_t b)
{
    uint16_t tempCORCON = CORCON;
    CORCON = 0x00e2;

    volatile int16_t result = a+b;

    CORCON = tempCORCON;
    return result;
}
''', '-c','-O1')
_add:
	lnk	#2
	mov	_CORCON,w2
	mov	#226,w3
	mov	w3,_CORCON
	add	w1,w0,w1
	mov	w1,[w15-2]
	mov	w2,_CORCON
	mov	[w15-2],w0
	ulnk
	return

Unfortunately this causes the compiler to put the sum on the stack rather than just stick it in the W0 register, as desired. It’非常难以说服编译器确实想要你想要的,并确保你已经正确完成了它,这就是为什么模式位可以是真正的痛苦,在这样的情况下。

您也可以尝试使用“barriers”强制编译器在其计算中承担某些依赖关系约束。这些基本上是内联组合的空块,它利用扩展程序集法来表达这些约束,但可以非常棘手,以确保您的代码是正确的。

pyxc16.compile(r'''
#include <stdint.h>

extern volatile uint16_t CORCON;

int16_t add(int16_t a, int16_t b)
{
    uint16_t tempCORCON = CORCON;
    CORCON = 0x00e2;

    asm volatile("" :"+r"(a));
    // don't actually do anything, but tell the compiler 
    // that the value of "a" might depend on this assembly code

    int16_t result = a+b;
    
    asm volatile("" ::"r"(result));
    // don't actually do anything, but tell the compiler 
    // that this assembly code might depend on the value of "result"
    
    CORCON = tempCORCON;
    return result;
}
''', '-c','-O1')
_add:
	mov	_CORCON,w2
	mov	#226,w3
	mov	w3,_CORCON
	add	w0,w1,w0
	mov	w2,_CORCON
	return

CORCON和定点

最后,XC16 C编译器用户第12.3节中的一个音符之一’s Guide is that the compiler also manages CORCON in some cases:

特殊的寄存器和由编译器管理的寄存器是:W0-W15, RCOUNT,状态(SR),PSVPAG和DSRPAG。如果启用了固定点支持,则 编译器可以分配A和B,在这种情况下,编译器可以调整CORCON。

In the code below, the compiler adds its own push _CORCONpop _CORCON instructions at the beginning and end of the function, but doesn’t seem to modify CORCON,而且 ordering of the computations get rearranged (the CORCON = tempCORCON translates into mov w2,_CORCON which executes before any of the _Fract/_Accum code even runs)

pyxc16.compile(r'''
#include <stdint.h>

extern volatile uint16_t CORCON;

int16_t baz(int16_t a, int16_t b)
{
    uint16_t tempCORCON = CORCON;
    CORCON = 0x00e2;

    _Fract af = a;
    _Fract bf = b;
    _Accum acc = 0;
    acc += af*bf;
    _Fract result = acc >> 15;
    
    CORCON = tempCORCON;
    return (int16_t)result;
}
''', '-c','-O1', '-menable-fixed')
_baz:
	push	_CORCON
	mov	_CORCON,w2
	mov	#226,w3
	mov	w3,_CORCON
	mov	w2,_CORCON
	cp0	w1
	mov	#0x8000,w2
	btsc	_SR,#0
	mov	#0x7FFF,w2
	btsc	_SR,#1
	clr	w2
	cp0	w0
	mov	#0x8000,w1
	btsc	_SR,#0
	mov	#0x7FFF,w1
	btsc	_SR,#1
	clr	w1
	mul.ss	w2,w1,w4
	sl	w4,w4
	rlc	w5,w0
	mov	w0,w1
	clr	w0
	asr	w1,#15,w2
	mov	#15,w3
	dec	w3,w3
	bra	n,.LE18
	asr.b	w2,w2
	rrc	w1,w1
	rrc	w0,w0
	bra	.LB18
.LE18:
	mov	w1,w1
	asr	w1,#15,w0
	pop	_CORCON
	return

The _Accum_Fract types have certain semantics as defined in ISO C提案N1169(扩展支持嵌入式处理器, 但是我’M不熟悉它们,为您提供有关如何使用它们的建议。

包起来

我们讨论了如何在内联汇编中与C编译器互动— basically don’T fases与由编译器管理的任何CPU资源,因为编译器始终假设关于CPU状态的某些事项。这些资源包括核心CPU寄存器和堆栈。

作为这种危险行为的比喻,我引用了2018年3月的交通致命性,涉及jaywalking行人和自驾驶车辆。不当的假设可以致命。大学教师’在编译器周围的jaywalk。请小心。


©2019年杰森M. Sachs,保留所有权利。


[]
评论 MR_BANDIT.2019年12月17日

优秀的文章!基本上:

  • 你不喜欢混淆编译器
  • 你应该了解优化结果
  • 你应该了解编译器动作可以通过修订来改变
  • 你不做愚蠢的东西

解决此问题的其他安全方法是在ASM中编写功能并控制所有内容。在c中写作的问题是结果是隐藏的 - 因为杰森清楚而专注地显示。 

这是可以采取的“技术” 天(如果一个人真的很幸运) 找到,可能甚至没有看过些什么 特别糟糕 (TM)发生 - 就像有人死亡一样。

杰森:做得好!

[]
评论 朱丽克2019年12月17日

我绝对同意你不应该用编译的资源混乱。我使用的编译器IAR实际上允许包含一个语法的内联汇编程序告诉编译器使用的是销毁的内容。即便如此,我从未发现在崩溃处理程序之外使用内联汇编程序的任何原因我编写了执行寄存器和堆栈转储。即使是特殊寄存器也具有由编译器提供的内部函数。我发现如果我需要优化速度代码,我可以在c中做到,优化器会产生比我在汇编程序中尝试执行的更好的代码(它知道几个技巧我没有)。

[]
评论 多勒弗勒2019年12月17日

优秀的杰森,谢谢!
同样,只有我被迫使用内联程序的时间仅适用于OS内部和崩溃转储。
然后,必须仔细检查装配输出以确保没有缺少障碍或其他错误......
再次感谢!

要发布回复评论,请单击连接到每个注释的“回复”按钮。发布新的评论(不是回复评论),请在评论的顶部查看“写评论”选项卡。

注册将允许您参加所有相关网站的论坛,并为您提供所有PDF下载。

注册

我同意 使用条款隐私政策.

尝试我们偶尔但流行的时事通讯。非常容易取消订阅。
或登录