当前位置:   article > 正文

原子操作 - ARM汇编同步机制实例(一)_strex arm汇编

strex arm汇编

为了实现线程间同步,一般都要在执行关键代码段之前加互斥(Mutex)锁,且在执行完关键代码段之后解锁。为了实现所谓的互斥锁的概念,一般都需要所在平台提供支持。本文主要用来说明ARM平台上特有的独占访问指令LDREX和STREX的工作原理,以及如何使用。而它们也是ARM平台上,实现互斥锁等线程同步工具的基础。

1. 硬件支持

引子

软件如何使用ARM V7之后的LDREX和STREX指令来实现spin lock和atomic 函数,这篇文章接着探讨ARM架构和总线协议如何来支持的。对于某款ARM处理器和总线CCI,CCN和CMN产品的具体实现,属于实现层面的微架构,一般需要NDA,这里不予讨论。

顺便提一下,在ARMv8 架构下对应的是LDXR (load exclusive register 和STXR (store exclusiveregister)及其变种指令,另外,在ARMv8.1架构中引入atomic instruction, 例如LDADD (Atomic add),CAS(Compare and Swap)等。

Exclusive monitor

首先,作为一个爱问为什么的工程师,一定会想到LDXR/ STXR和一般的LDR/STR有什么区别。这个区别就在于LDXR除了向memory发起load请求外,还会记录该memory所在地址的状态(一般ARM处理器在同一个cache line大小,也就是64 byte的地址范围内共用一个状态),那就是Open和Exclusive。

我们可以认为一个叫做exclusive monitor的模块来记录。根据CPU访问内存地址的属性(在页表里面定义),这个组件可能在处理器 L1 memory system, 处理器cluster level, 或者总线,DDR controller上。

下面是Arm ARM架构 [1] 文档定义的状态转换图

实例说明

1)CPU1发起了一个LDXR的读操作,记录当前的状态为Exclusive

2)CPU2发起了一个LDXR的读操作,当前的状态为Exclusive,保持不变

3)CPU2发起了一个STXR的写操作,状态从Exclusive变成Open,同时数据回写到DDR

4)CPU1发起了一个STXR的写操作,因为当前的exclusive monitor状态为Open,写失败(假如程序这时用STR操作来写,写会成功,但是这个不是原子操作函数的本意,属于编程错误)

假如有多个CPU,同时对一个处于Exclusive的memory region来进行写,CPU有内部逻辑来保证串行化。

Monitor的状态除了STXR会清掉,从Exclusive变成Open之外,还有其他因素也可以导致monitor的状态被清掉,所以软件在实现spinlock的时候,一般会用一个loop循环来实现,所谓“spin”。

Exclusive monitor实现所处的位置

根据LDXR/STXR 访问的memory的属性,需要的monitor可以在CPU内部,总线,也可以DDR controller(例如ARM DMC-400 [2]在每个memory interface 支持8个 exclusive access monitors)。

一般Memory属性配置为 normal cacheable, shareable,这种情形下,CPU发起的exclusive access会终结在CPU cluster内部,对外的表现,比如cacheline fill和line eviction和正常的读写操作产生的外部行为是一样的。具体实现上,需要结合local monitor的状态管理和cache coherency 的处理逻辑,比如MESI/MOESI的cacheline的状态管理来。

为方便大家理解,下面划出一个monitor在一个假象SOC里面的逻辑图(在一个真实芯片里面,不是所有monitor都会实现,需要和SOC vendor确认)

External exclusive monitor

对于normal non-cacheable,或者Device类型的memory属性的memory地址,cpu会发出exclusive access的AXI 访问(AxLOCK signals )到总线上去,总线需要有对应的External exclusive monitor支持,否则会返回错误。例如, 假如某个SOC不支持外部global exclusivemonitor,软件把MMU disabled的情况下,启动SMP Linux,系统是没法启动起来的,在spinlock处会挂掉。

AMBA AXI/ACE 规范

The exclusive access mechanism can provide semaphore-type operations without requiring the bus to remain dedicated to a particular master for the duration of the operation. This means the semaphore-type operations do not impact either the bus access latency or the maximum achievable bandwidth.

The AxLOCK signals select exclusive access, and the RRESP and BRESP signals indicate the success or failure of the exclusive access read or write respectively.

The slave requires additional logic to support exclusive access. The AXI protocol provides a mechanism to indicate when a master attempts an exclusive access to a slave that does not support it.

Atomic指令的支持

处理器,支持cache coherency协议的总线,或者DDR controller可以增加了一些简单的运算,比如,在读写指令产生的memory访问的过程中一并把简单的运算给做了。

AMBA 5 [3] 里面增加了对Atomic transactions的支持

AMBA 5 introduces Atomic transactions, which perform more than just a single access, and have some form of operation that is associated with the transaction.

Atomic transactions are suited to situations where the data is located a significant distance from the agent that must perform the operation. Previously, performing an operation that is atomically required pulling the data towards the agent, performing the operation, and then pushing the result back.

Atomic transactions enable sending the operation to the data, permitting the operation to be performed closer to where the data is located.

The key advantage of this approach is that it reduces the amount of time during which the data must be made inaccessible to other agents in the system.

支持4种Atomic transaction:AtomicStore ,AtomicLoad,AtomicSwap 和AtomicCompare

QA

1) Local monitor和Global monitor的使用场景

* Local monitor适用于访问的memory属为normal cacheable, shareable或者non-shareable的情况.

* Global monitor ,准确来说,external global exclusive monitor (处理器之外,在外部总线上)用于normal noncacheable或者device memory类型。比如可以用于一个Cortex-A处理器和一个Cortex-M 处理器(没有内部cache)之间的同步。

2)多CPU下,多个LDREX,和STREX的排他性实现

* 各个处理器和总线的实现不同,可以从软件上理解为和data coherency实现相结合,比如M(O)ESI协议[5],这是一种 Invalidate-based cache coherence protocol, 其中的key point就是当多个CPU在读同一个cacheline的时候,在每个CPU的内部cache里面都有cacheline allocation, cacheline的状态会变成Shared;但是当某个CPU做写的时候,会把其它CPU里面的cacheline数据给invalidate掉,然后写自己的cacheline数据,同时设置为Modified状态,从而保证了数据的一致性。

* LDREX,本质上是一个LDR,CPU1做cache linefill,然后设置该line为E状态(Exclusive),额外的一个作用是设置exclusive monitor的状态为Exclusive;其他cpu做LDREX,该line也会分配到它的内部cache里面,状态都设置为Shared ,也会设置本CPU的monitor的状态。当一个CPU 做STREX时候,这个Write操作会把其它CPU里面的cacheline数据给invalidate掉。同时也把monitor的状态清掉,从Exclusive变成Open的状态,这个MESI协议导致cachline的状态在多CPU的变化,是执行Write操作一次性改变的。这样在保证数据一致性的同时,也保证了montitor的状态更新同步改变。

3)比如举一个多核的场景,一个核ldrex了,如果本核的local monitor会发生什么,外部的global monitor发生什么,开不开mmu,cache不cache,区别和影响是什么。

Ldrex/strex本来就是针对多核的场景来设计的,local monitor的状态发生改变,不会影响外部的global monitor状态。但是external global monitor的状态发生改变,可以告诉处理器,把local monitor的状态清掉。

Data coherency是通过硬件来支持的。对于normal cacheable类型的memory, MMU和DCache必须使能,否则CPU会把exclusive类型的数据请求发出处理器,这时需要外部monitor的支持。

参考文献

[1] ARM Architecture Reference Manual ARMv8,

https://developer.arm.com/docs/ddi0487/latest/arm-architecture-reference-manual-armv8-for-armv8-a-architecture-profile

[2] DMC400 TRM

https://developer.arm.com/products/system-design/fast-models/docs/dui1087/latest/corelink-dmc-400-dynamic-memory-controller-user-guide

[3] AMBA specification ( 需要注册用户名,一般个人邮件地址都可以),

https://developer.arm.com/products/architecture/system-architectures/amba

[4] Arm training courses

https://developer.arm.com/support/training

其他架构的实现探讨

X86_64

看文档和代码感觉类似ARM的atomic instruction, 比如 CMPXCHG(Compare andexchange)

arch/x86/include/asm/spinlock.h

Onx86, we implement read-write locks using the generic qrwlock with x86 specific optimization.

include/asm-generic/qspinlock.h

RISCV (Volume II: RISC-V Privileged ArchitecturesV1.10)

对RISCV了解不多,和ARM相比,同为RISC架构,对原子指令的支持也比较类似

Support for atomicinstructions is divided into two categories:

1) Load-Reserved/Store-Conditional (LR/SC)instructions have been added in the atomic instruction extension.

2) Within AMOs, there are four levels of support: AMONone, AMOSwap, AMOLogical, and AMOArithmetic.

AMONone indicates that no AMO operations are supported.

AMOSwap indicates that only amoswap instructions are supportedin this address range. AMOLogical indicates that swap instructions plus all thelogical AMOs (amoand, amoor, amoxor) are supported.

AMOArithmetic indicates that all RISC-V AMOs are supported.

arch/riscv/include/asm/spinlock.h

2. ldrex/strex

2.1. ldrex与strex大概处理流程

在arm系统当中通过LDREX和STREX实现内存的原子操作,首先研究一下两条指令的语义。其实LDREX和STREX指令,是将单纯的更新内存的原子操作分成了两个独立的步骤。大致的流程如下,但是ARM内部为了实现这个功能,还有不少复杂的情况要处理。

  1. LDREX用来读取内存中的值,并标记对该段内存的独占访问:LDREX Rx, [Ry]。上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。
  2. 而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:STREX Rx, Ry, [Rz]。如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx的值设置成1。一旦某条STREX指令执行成功后,以后再对同一段内存尝试使用STREX指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。

2.2. ARM内存独享与共享

在ARM系统中,内存有两种不同且对立的属性,即共享(Shareable)和非共享(Non-shareable)。共享意味着该段内存可以被系统中不同处理器访问到,这些处理器可以是同构的也可以是异构的。而非共享,则相反,意味着该段内存只能被系统中的一个处理器所访问到,对别的处理器来说不可见。为了实现独占访问,ARM系统中还特别提供了所谓独占监视器(Exclusive Monitor)的东西,其结构大致如下:

b0a99415354e5e39f4a797379ced7e59.png

可以看出来,一共有两种类型的独占监视器。每一个处理器内部都有一个本地监视器(Local Monitor),且在整个系统范围内还有一个全局监视器(Global Monitor)。如果要对非共享内存区中的值进行独占访问,只需要涉及本处理器内部的本地监视器就可以了;而如果要对共享内存区中的内存进行独占访问,除了要涉及到本处理器内部的本地监视器外,由于该内存区域可以被系统中所有处理器访问到,因此还必须要由全局监视器来协调。

对于本地监视器来说,它只标记了本处理器对某段内存的独占访问,在调用LDREX指令时设置独占访问标志,在调用STREX指令时清除独占访问标志。而对于全局监视器来说,它可以标记每个处理器对某段内存的独占访问。也就是说,当一个处理器调用LDREX访问某段共享内存时,全局监视器只会设置针对该处理器的独占访问标记,不会影响到其它的处理器。当在以下两种情况下,会清除某个处理器的独占访问标记:

1)当该处理器调用LDREX指令,申请独占访问另一段内存时;

2)当别的处理器成功更新了该段独占访问内存值时。

对于第二种情况,也就是说,当独占内存访问内存的值在任何情况下,被任何一个处理器更改过之后,所有申请独占该段内存的处理器的独占标记都会被清空。另外,更新内存的操作不一定非要是STREX指令,任何其它存储指令都可以。但如果不是STREX的话,则没法保证独占访问性。现在的处理器基本上都是多核的,一个芯片上集成了多个处理器。而且对于一般的操作系统,系统内存基本上都被设置上了共享属性,也就是说对系统中所有处理器可见。因此,我们这里主要分析多核系统中对共享内存的独占访问的情况。为了更加清楚的说明,我们可以举一个例子。假设系统中有两个处理器内核,而一个程序由三个线程组成,其中两个线程被分配到了第一个处理器上,另外一个线程被分配到了第二个处理器上。且他们的执行序列如下:

4c8335b634451efcd2e1f9b4f32db294.png

大致经历的步骤如下:

  1. CPU2上的线程3最早执行LDREX,锁定某段共享内存区域。它会相应更新本地监视器和全局监视器。
  2. 然后,CPU1上的线程1执行LDREX,它也会更新本地监视器和全局监视器。这时在全局监视器上,CPU1和CPU2都对该段内存做了独占标记。
  3. 接着,CPU1上的线程2执行LDREX指令,它会发现本处理器的本地监视器对该段内存有了独占标记,同时全局监视器上CPU1也对该段内存做了独占标记,但这并不会影响这条指令的操作。
  4. 再下来,CPU1上的线程1最先执行了STREX指令,尝试更新该段内存的值。它会发现本地监视器对该段内存是有独占标记的,而全局监视器上CPU1也有该段内存的独占标记,则更新内存值成功。同时,清除本地监视器对该段内存的独占标记,还有全局监视器所有处理器对该段内存的独占标记。
  5. 下面,CPU2上的线程3执行STREX指令,也想更新该段内存值。它会发现本地监视器拥有对该段内存的独占标记,但是在全局监视器上CPU1没有了该段内存的独占标记(前面一步清空了),则更新不成功。
  6. 最后,CPU1上的线程2执行STREX指令,试着更新该段内存值。它会发现本地监视器已经没有了对该段内存的独占标记(第4步清除了),则直接更新失败,不需要再查全局监视器了。

所以,可以看出来,这套机制的精髓就是,无论有多少个处理器,有多少个地方会申请对同一个内存段进行操作,保证只有最早的更新可以成功,这之后的更新都会失败。失败了就证明对该段内存有访问冲突了。实际的使用中,可以重新用LDREX读取该段内存中保存的最新值,再处理一次,再尝试保存,直到成功为止。

还有一点需要说明,LDREX和STREX是对内存中的一个字(Word,32 bit)进行独占访问的指令。如果想独占访问的内存区域不是一个字,还有其它的指令。它们必须配对使用,不能混用:

  1. LDREXB和STREXB:对内存中的一个字节(Byte,8 bit)进行独占访问;
  2. LDREXH和STREXH:中的一个半字(Half Word,16 bit)进行独占访问;
  3. LDREXD和STREXD:中的一个双字(Double Word,64 bit)进行独占访问。


2. arm32的实现

arm32分架构有不同的实现,source code:arch/arm/include/asm/atomic.h。我抽出来一部分分析,如下:注意#asm_op作为一个通用的操作符,可以是add,sub等等.下面分析是add的实现过程:

  1. #define ATOMIC_INIT(i) { (i) }
  2. #ifdef __KERNEL__
  3. /*
  4. * On ARM, ordinary assignment (str instruction) doesn't clear the local
  5. * strex/ldrex monitor on some implementations. The reason we can use it for
  6. * atomic_set() is the clrex or dummy strex done on every exception return.
  7. */
  8. #define atomic_read(v) READ_ONCE((v)->counter)
  9. #define atomic_set(v,i) WRITE_ONCE(((v)->counter), (i))
  10. #if __LINUX_ARM_ARCH__ >= 6 -----------------------(1)
  11. /*
  12. * ARMv6 UP and SMP safe atomic ops. We use load exclusive and
  13. * store exclusive to ensure that these are atomic. We may loop
  14. * to ensure that the update happens.
  15. */
  16. #define ATOMIC_OP(op, c_op, asm_op) \
  17. static inline void atomic_##op(int i, atomic_t *v) \
  18. { \
  19. unsigned long tmp; \
  20. int result; \
  21. \
  22. prefetchw(&v->counter);-------------------------(2) \
  23. __asm__ __volatile__("@ atomic_" #op "\n" \-------------(3)
  24. "1: ldrex %0, [%3]\n" \--------------(4
  25. " " #asm_op " %0, %0, %4\n" \------------(5
  26. " strex %1, %0, [%3]\n" \------------(6
  27. " teq %1, #0\n" \-----------------(7
  28. " bne 1b" \-----------------------------------(8)
  29. : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) \
  30. : "r" (&v->counter), "Ir" (i) \
  31. : "cc"); \
  32. } \
  33. #else /* ARM_ARCH_6 */
  34. #ifdef CONFIG_SMP
  35. #error SMP not supported on pre-ARMv6 CPUs
  36. #endif
  37. #define ATOMIC_OP(op, c_op, asm_op) \
  38. static inline void atomic_##op(int i, atomic_t *v) \
  39. { \
  40. unsigned long flags; \
  41. \
  42. raw_local_irq_save(flags); \
  43. v->counter c_op i; \
  44. raw_local_irq_restore(flags); \
  45. } \
  46. #endif /* __LINUX_ARM_ARCH__ */

对上面的1234567分别分析如下:
(1). ARMv6之前的CPU并不支持SMP,之后的ARM架构都是支持SMP的(例如我们熟悉的ARMv7-A)。因此,对于ARM处理,其原子操作分成了两个阵营,一个是支持SMP的ARMv6之后的CPU,另外一个就是ARMv6之前的,只有单核架构的CPU。对于UP,原子操作就是通过关闭CPU中断来完成的。

(2). 这里的代码和preloading cache相关。在strex指令之前将要操作的memory内容加载到cache中可以显著提高性能。

(3). 为了完整性,我还是重复一下汇编嵌入c代码的语法:嵌入式汇编的语法格式是:asm(code : output operand list : input operand list : clobber list)。output operand list 和 input operand list是c代码和嵌入式汇编代码的接口,clobber list描述了汇编代码对寄存器的修改情况。为何要有clober list?我们的c代码是gcc来处理的,当遇到嵌入汇编代码的时候,gcc会将这些嵌入式汇编的文本送给gas进行后续处理。这样,gcc需要了解嵌入汇编代码对寄存器的修改情况,否则有可能会造成大麻烦。例如:gcc对c代码进行处理,将某些变量值保存在寄存器中,如果嵌入汇编修改了该寄存器的值,又没有通知gcc的话,那么,gcc会以为寄存器中仍然保存了之前的变量值,因此不会重新加载该变量到寄存器,而是直接使用这个被嵌入式汇编修改的寄存器,这时候,我们唯一能做的就是静静的等待程序的崩溃。还好,在output operand list 和 input operand list中涉及的寄存器都不需要体现在clobber list中(gcc分配了这些寄存器,当然知道嵌入汇编代码会修改其内容),因此,大部分的嵌入式汇编的clobber list都是空的,或者只有一个cc,通知gcc,嵌入式汇编代码更新了condition code register。

对着上面的code就可以分开各段内容了。@符号标识该行是注释。

这里的__volatile__主要是用来防止编译器优化的。也就是说,在编译该c代码的时候,如果使用优化选项(-O)进行编译,对于那些没有声明__volatile__的嵌入式汇编,编译器有可能会对嵌入c代码的汇编进行优化,编译的结果可能不是原来你撰写的汇编代码,但是如果你的嵌入式汇编使用__asm__ volatile(嵌入式汇编)的语法格式,那么也就是告诉编译器,不要随便动我的嵌入汇编代码。

(4). 我们先看ldrex和strex这两条汇编指令的使用方法。ldr和str这两条指令大家都是非常的熟悉了,后缀的ex表示Exclusive,是ARMv7提供的为了实现同步的汇编指令。

LDREX , []

是base register,保存memory的address,LDREX指令从base register中获取memory address,并且将memory的内容加载到(destination register)中。这些操作和ldr的操作是一样的,那么如何体现exclusive呢?其实,在执行这条指令的时候,还放出两条“狗”来负责观察特定地址的访问(就是保存在[]中的地址了),这两条狗一条叫做local monitor,一条叫做global monitor。

STREX , , []

和LDREX指令类似,是base register,保存memory的address,STREX指令从base register中获取memory address,并且将 (source register)中的内容加载到该memory中。这里的保存了memeory 更新成功或者失败的结果,0表示memory更新成功,1表示失败。STREX指令是否能成功执行是和local monitor和global monitor的状态相关的。对于Non-shareable memory(该memory不是多个CPU之间共享的,只会被一个CPU访问),只需要放出该CPU的local monitor这条狗就OK了,下面的表格可以描述这种情况

(5)当其中一个进程(同一个CPU或者不同CPU)数据已经被strex以后,下一个过来的数据再执行必然不会成功,返回1表示更新失败。因此,(7)/(8)两个步骤组合发现数据更新失败就从函数最开始从新执行ldrex命令再次进行数据更新。这种规则保证这种资源的更新的原子性,不会出现多出重复更新的情况。

thread 1thread 2local monitor的状态
Open Access state
LDREXExclusive Access state
LDREXExclusive Access state
ModifyExclusive Access state
STREXOpen Access state
ModifyOpen Access state
STREX在Open Access state的状态下,执行STREX指令会导致该指令执行失败
保持Open Access state,直到下一个LDREX指令

开始的时候,local monitor处于Open Access state的状态,thread 1执行LDREX 命令后,local monitor的状态迁移到Exclusive Access state(标记本地CPU对xxx地址进行了LDREX的操作),这时候,中断发生了,在中断handler中,又一次执行了LDREX ,这时候,local monitor的状态保持不变,直到STREX指令成功执行,local monitor的状态迁移到Open Access state的状态(清除xxx地址上的LDREX的标记)。返回thread 1的时候,在Open Access state的状态下,执行STREX指令会导致该指令执行失败(没有LDREX的标记,何来STREX),说明有其他的内核控制路径插入了。
对于shareable memory,需要系统中所有的local monitor和global monitor共同工作,完成exclusive access,概念类似,这里就不再赘述了。大概的原理已经描述完毕,下面回到具体实现面。

"1:    ldrex    %0, [%3]\n"  

其中%3就是input operand list中的"r" (&v->counter),r是限制符(constraint),用来告诉编译器gcc,你看着办吧,你帮我选择一个通用寄存器保存该操作数吧。%0对应output openrand list中的"=&r" (result),=表示该操作数是write only的,&表示该操作数是一个earlyclobber operand,具体是什么意思呢?编译器在处理嵌入式汇编的时候,倾向使用尽可能少的寄存器,如果output operand没有&修饰的话,汇编指令中的input和output操作数会使用同样一个寄存器。因此,&确保了%3和%0使用不同的寄存器。

(5). 完成步骤(4)后,%0这个output操作数已经被赋值为atomic_t变量的old value,毫无疑问,这里的操作是要给old value加上i。这里%4对应"Ir" (i),这里“I”这个限制符对应ARM平台,表示这是一个有特定限制的立即数,该数必须是0~255之间的一个整数通过rotation的操作得到的一个32bit的立即数。这是和ARM的data-processing instructions如何解析立即数有关的。每个指令32个bit,其中12个bit被用来表示立即数,其中8个bit是真正的数据,4个bit用来表示如何rotation。更详细的内容请参考ARM文档。

(6). 这一步将修改后的new value保存在atomic_t变量中。是否能够正确的操作的状态标记保存在%1操作数中,也就是"=&r" (tmp)。

(7). 检查memory update的操作是否正确完成,如果OK,皆大欢喜,如果发生了问题(有其他的内核路径插入),那么需要跳转到lable 1那里,从新进行一次read-modify-write的操作。

四 arm64的实现

对于arm64的source code如下(arch/arm64/include/asm/atomic_lse.h):

  1. static inline void atomic_add(int i, atomic_t *v)
  2. {
  3. register int w0 asm ("w0") = i; --------------------(1
  4. register atomic_t *x1 asm ("x1") = v; -----------------(2
  5. asm volatile(ARM64_LSE_ATOMIC_INSN(__LL_SC_ATOMIC(add),
  6. " stadd %w[i], %[v]\n") ----------------------(3
  7. : [i] "+r" (w0), [v] "+Q" (v->counter) -----------------(4
  8. : "r" (x1) ----------------------(5
  9. : __LL_SC_CLOBBERS); ----------------------------(6
  10. }

(1). 将变量i放入寄存器w0中
(2). 将atomic_t结构体变量v放入寄存器x1中
(3). 标记两个变量相加,即汇编指令
(4). 到底哪两个变量相加? 解析寄存器数值,并执行相加操作.即输出操作数列表
(5). 将相加的数值存入寄存器x1中,相当于在原来v的基础上进行v->counter + i的操作.即输入操作数列表
(6). 改变资源列表,即告诉编辑器哪些资源被修改,需要去update

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/712800
推荐阅读
相关标签
  

闽ICP备14008679号