ARM下书写位置无关的代码

(以后应该再也不会写汇编了,将以前总结的东西发给大家参考)


位置无关代码,即该段代码无论放在内存的哪个地址,都能正确运行。究其原因,是因为代码里
没有使用绝对地址,都是相对地址。

位置无关的写法:

(1) B指令

B指令接受一个相对地址,因此在汇编里用B跳转到一个标号时,实际编译的结果是一个相对跳转。
相对地址有个范围限制,即目标不能太远,一般目标放在同一个文件里是肯定可以的。
_start:
    b  _reset
_reset:
      ...

(2) BL

BL用于调用函数,也是一个相对跳转

(3) ADR

获取标号的地址,在编译时会使用PC+偏移的方式得到该位置的地址。例如,当TEXT_BASE是0时
SMRDATA可能被放在0x100的位置,当TEXT_BASE为0x30000000时放在0x30000100的位置。使用ADR
总能获取正确的位置,与程序的加载地址无关。
    ADR R0, SMRDATA
SMRDATA:
    .word  0x22111120 
    .word  0x00002F50 
    .word  0x00000700 
(相应的, LDR Rn, =LABEL是位置相关的)

(4) LDR

当加标号时,LDR可以用于伪指令,也可以真指令。
真指令: (标号前不加=号,表示取标号处的值)
    LDR R0,  SDRDATA
实际被编译为LDR R0, [PC, #NN],其中NN是目标的相对距离

伪指令: (标号前加=号,取标号的地址)
    LDR R0, = SDRDATA
实际编译的时候的时候,会在某位置存处SDRDATA的值,然后用一个LDR取出来。
显然,用LDR时,加不加=号有很大区别。
无=号:取该标号处的值,位置无关
有=号:取该标号的地址,位置相关

举例分析

例1:中断向量跳转

_start:    
    b       reset
    ldr    pc, _undefined_instruction
    ldr    pc, _software_interrupt
    ldr    pc, _prefetch_abort
    ldr    pc, _data_abort
    ldr    pc, _not_used
    ldr    pc, _irq
    ldr    pc, _fiq

_undefined_instruction:    .word undefined_instruction
_software_interrupt:    .word software_interrupt
_prefetch_abort:    .word prefetch_abort
_data_abort:        .word data_abort
_not_used:        .word not_used
_irq:            .word irq
_fiq:            .word fiq
其中,
ldr pc, _irq,由于没加=号,表示取值_irq处的值放在pc里 (位置无关)
_irq:  .word irq ,表示_irq存放的值是irq的绝对地址(位置有关)

例2:

bl  main ; 位置无关
ldr pc, =main; 把main的地址放在pc,位置相关

例3: 静态变量

_MAGIC_NUM:
    .word 0x12345678
取值
    LDR  R0, _MAGIC_NUM  ; 位置无关

例4: 存放标号绝对地址(绝对地址是编译的时候已经固定)

_OS_Running_p:
    .word  OS_Runing
则_OS_Running_p存放的是标号OS_Running的绝对地址

例5: 显式LDR和隐式LDR

以给某C中的变量的g_num赋值为例
(1) 使用伪指令LDR,即为隐式
    LDR  R0, =g_num    @取g_num的地址到R0
    MOV R1, #10
            STR  R1, R0
(2) 显式赋值
先定义一个变量p_g_num,用于保存g_num的地址
p_g_num:
    .word   g_num   @ g_num的绝对地址
然后赋值
    LDR R0, p_g_num
    MOV R1, #10
    STR R1,  R0
显然,两者其实一样,伪指令被展开后其实就是(2)的样子。
不同点在于:在多次引用的时候,如果使用伪指令,则会有多个临时定义。所以,
在多次引用的时候应该使用显式定义。

例6: 使用LinkScript中的变量

这种情形和例5相同
1) LinkScript中定义了两个位置
{
    __bss_start = .;
    .bss : { *(.bss) }
    _end = .;
}
2) 定义两个变量,用于存处这两个位置
.globl _bss_start
_bss_start:
    .word __bss_start

.globl _bss_end
_bss_end:
    .word _end
3) 使用这两个位置
    ldr    r0, _bss_start        /* find start of bss segment        */
    ldr    r1, _bss_end        /* stop here                        */

<think>我们正在讨论ARM架构中的POP指令行为。根据用户的问题,他想知道在POP指令中寄存器列表的书写顺序是否会影响数据出栈的顺序。 根据ARM架构的规范,POP指令实际上是LDMIA(Load Multiple Increment After)指令的别名。在LDMIA指令中,寄存器列表的存储顺序是由寄存器编号决定的,而不是由书写顺序决定的。具体来说: - 当执行POP(或LDMIA)时,处理器会从低地址到高地址依次加载数据到寄存器列表中。 - 寄存器列表中的寄存器编号决定了加载的顺序:编号小的寄存器总是先被加载(即对应低地址的数据),编号大的寄存器后被加载(对应高地址的数据)。 因此,无论我们在指令中如何书写寄存器列表(例如写成{R0, R1}或{R1, R0}),处理器都会按照寄存器编号从小到大的顺序进行加载。也就是说,编号最小的寄存器将得到栈顶(当前SP指向的位置)的数据,然后SP递增,接着加载下一个编号较大的寄存器,依此类推。 需要注意的是,栈在ARM中通常是满递减栈(FD),即栈指针指向最后一个压入的数据,并且栈向低地址方向增长。因此,POP操作是从当前SP指向的位置(低地址)开始,向高地址依次加载数据。 举个例子: 假设当前栈顶(SP)指向0x1000,栈中从0x1000开始依次存储了R0、R1的值(即0x1000处是R0的值,0x1004处是R1的值,因为每个寄存器4字节)。 执行指令 `POP {R0, R1}` 时: - 首先,将0x1000处的数据加载到R0(因为R0编号比R1小)。 - 然后,将0x1004处的数据加载到R1。 - 最后,SP会增加8(两个寄存器),指向0x1008。 而如果执行指令 `POP {R1, R0}`,处理器的行为完全一样: - 仍然先加载0x1000处的数据到R0(因为R0编号小,即使写在后面),然后加载0x1004处的数据到R1。 - 这样就会导致原本栈顶的数据(R0的值)被加载到了R0,而下一个数据(R1的值)被加载到了R1,与书写顺序无关。 但是,这里有一个重要的陷阱:如果我们在同一个POP指令中,指定了两个相同的寄存器(这是不允许的)或者有重叠(比如同时指定了R0和R0),那么行为是未定义的。另外,如果寄存器列表中有多个寄存器,它们的编号顺序决定了加载顺序,而书写顺序不影响。 然而,如果我们在同一个POP指令中,指定了多个寄存器,并且这些寄存器的编号顺序和书写顺序不同,那么实际加载的顺序仍然是按编号顺序。这可能会导致一个常见错误:例如,我们想先弹出到R1再弹出到R0,于是写了`POP {R1, R0}`,但实际执行时却是先加载到R0再加载到R1。这样,R0得到的是栈顶的数据,而R1得到的是下一个数据,但如果我们原本希望R1得到栈顶数据,R0得到下一个数据,那么就会出错。 因此,结论是:POP指令中寄存器列表的书写顺序并不决定出栈顺序,真正决定出栈顺序的是寄存器编号的顺序(从小到大)。所以,程序员在编写POP指令时,必须注意寄存器编号的顺序,而不是书写顺序。 引用支持: 在引用[1]中,有关于栈操作(PUSH和POP)的说明,其中提到:“PUSH指令实际是STMDB指令的别名,POP指令是LDMIA指令的别名。在ARM中,多寄存器存储和加载指令的寄存器列表顺序在硬件层面会被重新排序,按照寄存器编号的顺序进行存储或加载。” [^1] 引用[3]中也提到:“POP指令(即LDMIA)会按照寄存器编号从小到大的顺序依次从栈中弹出数据。” [^3] 因此,用户需要特别注意,在编写POP指令时,寄存器列表的书写顺序并不影响实际加载顺序,实际顺序由寄存器编号决定。为了避免错误,建议按照寄存器编号从小到大的顺序书写,这样代码可读性更好。 下面用两个例子说明: 例1:正确使用 假设栈中有两个值,我们希望将它们分别弹出到R0和R1(R0得到栈顶的值,R1得到下一个值),那么我们应该写: POP {R0, R1} // 或者 POP {R1, R0} ? 但根据规则,无论怎么写,都是R0先被加载(因为编号小),所以实际上我们只能通过选择寄存器来达到目的。如果我们希望R0得到栈顶的值,那么我们就用R0(因为它是编号最小的寄存器,会得到栈顶的值)。如果我们希望R1得到栈顶的值,那么我们不能同时使用R0,因为R0编号更小,会先被加载。所以我们可以单独弹R1,然后再弹下一个寄存器,或者使用其他编号比R1大的寄存器(如R2)作为第二个寄存器,但这样R1就是编号最小的了。 所以,如果我们希望R1得到栈顶的值,R0得到下一个值,我们可以这样写: POP {R1} // 弹出栈顶到R1,SP增加4 POP {R0} // 再弹出下一个到R0 或者,如果我们想用一条指令,那么寄存器列表不能包含R0(因为R0编号比R1小,会先被加载)。但是,如果我们使用一个编号比R1大的寄存器(比如R2)和R1一起弹出,那么R1就会是编号最小的,所以: POP {R1, R2} // 这样R1得到栈顶的值,R2得到下一个值 但是这样R0就没有被加载,所以如果我们需要R0,那么只能分两次弹出。 例2:错误示例 POP {R1, R0} // 本意:栈顶的值给R1,下一个给R0 实际效果:栈顶的值给R0,下一个给R1(因为R0编号比R1小,先加载R0) 因此,为了避免混淆,建议按照编号从小到大的顺序书写寄存器列表,这样代码阅读起来更清晰,而且实际执行顺序与书写顺序一致。 总结: 在ARM架构中,POP指令的寄存器列表书写顺序并不决定数据出栈顺序,实际顺序由寄存器编号从小到大决定。因此,程序员必须根据寄存器编号来安排寄存器的使用,以避免数据加载到错误的寄存器。 相关问题: 1. 在ARM架构中,PUSH指令的寄存器列表顺序是否影响入栈顺序? 2. 如何正确使用POP指令恢复多个寄存器的值? 3. 在中断处理中,ARM处理器自动保存寄存器的顺序是什么? 4. 为什么ARM架构要设计成按寄存器编号顺序进行多寄存器加载和存储? 5. 在编写汇编函数时,如何正确保存和恢复寄存器以避免寄存器覆盖?</think>在 ARM 架构中,**POP 指令的寄存器书写顺序不会影响数据出栈的实际顺序**。出栈顺序由以下核心规则决定: ### 1. **硬件强制排序机制 - POP 指令本质是 `LDMIA sp!, {reglist}` 的别名,其**物理加载顺序由寄存器编号决定**(低编号→高编号),与书写顺序无关[^1][^3]。 - 无论写成 `POP {R0,R1}` 或 `POP {R1,R0}`,处理器始终按编号**从小到大**加载: $$ \text{最小编号寄存器} \rightarrow \text{次小编号} \rightarrow \cdots \rightarrow \text{最大编号} $$ 例如: ```assembly POP {R1, R0} // 实际执行:先加载到R0(编号0),再加载到R1(编号1) ``` ### 2. **栈模型与数据加载规则 - ARM 默认使用**满递减栈(FD)**: - POP 操作从当前 SP 指向的地址(栈顶)开始加载 - 数据加载顺序:**低地址 → 高地址**(与入栈顺序相反)[^1][^3] - 内存布局示例(初始 SP=0x1000): | 地址 | 存储数据 | POP 加载顺序 | |--------|----------|--------------| | 0x0FFC | R12 的值 | 最后加载 | | 0x0FF8 | R3 的值 | → | | 0x0FF4 | R2 的值 | → | | 0x0FF0 | R1 的值 | → | | 0x0FEC | R0 的值 | **最先加载** | ### 3. **关键影响与常见错误 - **寄存器覆盖风险**: ```assembly POP {R0, R1} // 正确:R0←栈顶(0x0FEC), R1←0x0FF0 POP {R1, R0} // 危险!R0←栈顶(0x0FEC), R1←0x0FF0 // 若R0参与后续计算,值会被意外覆盖 ``` - **正确实践**: - 按编号顺序书写:`POP {R0-R3,R12}`(可读性更佳) - 避免寄存器逆序书写(可能引发逻辑错误)[^1] ### 4. **与 PUSH 指令的对称性 | 操作 | 实际物理顺序 | 书写顺序影响 | |--------|----------------------|--------------| | PUSH | 高编号→低编号(降序)| 无 | | **POP**| **低编号→高编号(升序)** | **无** | PUSH/POP 构成对称操作,确保上下文正确保存/恢复[^5]。 > ⚠️ **关键结论**:POP 指令的数据加载顺序**仅由寄存器编号决定**。错误书写顺序(如 `POP {R1,R0}`)不会改变执行顺序,但会导致寄存器赋值错位,引发严重逻辑错误[^1][^3]。 --- ### 相关问题 1. **在中断处理中,ARM 自动出栈的顺序是否与 POP 指令一致?** > 是!硬件异常返回时自动执行 `LDMIA`,按 R0→R1→...→PC 顺序恢复上下文[^3][^5]。 2. **POP 指令能否指定逆序寄存器(如 {R3,R2,R1})?** > 可以执行,但实际加载顺序仍是 R1→R2→R3(按编号升序),可能导致数据错位[^1]。 3. **如何安全恢复非连续寄存器(如 R0 和 R2)?** > 需分步操作避免覆盖: > ```assembly > POP {R0} // 先加载R0 > POP {R2} // 再加载R2 > ``` 4. **向量寄存器(NEON)的 VPOP 是否遵循相同规则?** > 是!`VPOP {D0-D3}` 按 D0→D1→D2→D3 顺序加载(编号升序)[^4]。 5. **POP 指令执行后栈指针(SP)如何变化?** > 每加载一个寄存器,SP 增加寄存器宽度(32位:+4字节),最终指向新栈顶[^1][^3]。 [^1]: 寄存器列表在硬件层面按编号重排序,加载顺序固定为升序。 [^3]: 栈模型(FD)强制加载顺序从低地址开始,对应编号最小寄存器。 [^5]: POP 与 PUSH 的对称性确保函数调用/返回时上下文正确恢复。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值