1:stm32中有3条总线:地址总线,数据总线,系统总线;地址总线上是地址值,数据总线上是数据值,cpu读写时会先在地址总线上传输地址值,如果是写操作之后数据总线上会放往前面地址处写入的数据值,如果是读操作之后数据总线上会放前面地址处的数据值;地址总线和数据总线只能访问rom,而系统总线既可以访问RAM也可以访问ROM,而当前访问RAM和ROM的地址会被寄存器SP和PC记录下来,即SP和PC就是用来记录当前RAM和ROM的访问地址/位置,访问ram必须以sp值作为基地址,访问rom必须以pc值作为基地址;psp和msp是访问ram的基址寄存器,psp和map可以访问同一地址的ram,只是一般2者会指向不同的ram地址。
2:SP MSP PSP的理解:
参考 1:https://blog.csdn.net/u012351051/article/details/125107815
2: https://mp.weixin.qq.com/s?__biz=MjM5MzUxMTAwMg==&mid=2649785048&idx=2&sn=8f3c5ac9cd4d4586d1543cb96502634d&chksm=be91c1ed89e648fb6c082a77f18b6bcffafc730c0041f1de3d50f3b53b0d7a37a170a55f17f9&scene=27
在stm32CPU中,堆栈寄存器SP一共有2个,分别为MSP、PSP。SP是对外使用的寄存器,或者认为SP始终指向当前模式下使用的MSP/PSP,只不过在OS内核或Handler模式下,SP先指向MSP,或者说SP拷贝了MSP的值,可以直接访问主堆栈。而在线程模式下,SP拷贝了PSP的值,可以直接访问线程(任务)堆栈。即SP是MSP和PSP的代言人,即SP是MSP和PSP的逻辑地址,对于裸机程序,我们只需要知道SP即可,而对于OS系统,尤其涉及中断、任务上下文切换时,就需要知道PSP和MSP了,OS底层也会直接针对PSP进行编程。
在发生中断时,CPU硬件需要自动保存现场,此时SP指向MSP还是PSP,取决于发生中断前正在使用的堆栈,即如果发生中断前,正在运行线程模式,而且是task任务运行,则使用PSP,而如果进入中断前,使用的MSP,这里就继续使用MSP。一旦开始执行中断服务程序,即handler模式,则一定使用MSP,也就是,如果是有OS的情况下,正在运行task的时候,发生了中断,CPU先使用PSP自动保存现场,跳入到中断服务程序后,就从PSP切换到MSP,所有中断服务程序用到的临时变量都存放到MSP里。
只有中断服务函数ISR工作在处理模式/handler下,其余所有的代码包括os的内核、驱动、应用等都工作在线程模式/thread下;而线程模式又分为特权状态(也叫内核态)和非特权状态(也叫用户态),之所以分成两个态就是为了os设计的;os的内核代码运行在线程模式的特权态下,用户app代码运行在线程模式下的非特权态。
32位ARM和Thumb指令:32位Thumb指令具有.w后缀。
LDR R0 [a]; 从a变量地址值处取出值传入R0中,等同于把a的值传入R0中,这里的a指变量a的地址值;
LDR 指令码 中 =后跟标号,取得是标号的地址,而不是标号的值,如果是一个具体的数值,那这个值是个地址值。
只有在进/出中断的两个节点上才会发生硬件的自动压/出栈,函数调用不会发生;同时硬件自动压/出栈不会在汇编文件中体现,因为是硬件自动完成的,不是受软件代码控制的;
虽然函数调用不会发生硬件自动压栈,但调用函数的过程是需要保护栈的,因此有些情况下需要用户软件压栈,这里不要混淆。
(gdb) disassemble
Dump of assembler code for function Reset_Handler:
0x08012090 <+0>: ldr.w sp, [pc, #52] ; 0x80120c8 <LoopFillZerobss+18>
0x08012094 <+4>: ldr r0, [pc, #52] ; (0x80120cc <LoopFillZerobss+22>)
0x08012096 <+6>: ldr r1, [pc, #56] ; (0x80120d0 <LoopFillZerobss+26>)
0x08012098 <+8>: ldr r2, [pc, #56] ; (0x80120d4 <LoopFillZerobss+30>)
0x0801209a <+10>: movs r3, #0
0x0801209c <+12>: b.n 0x80120a4 <Reset_Handler+20>
0x0801209e <+14>: ldr r4, [r2, r3]
0x080120a0 <+16>: str r4, [r0, r3]
0x080120a2 <+18>: adds r3, #4
0x080120a4 <+20>: adds r4, r0, r3
0x080120a6 <+22>: cmp r4, r1
0x080120a8 <+24>: bcc.n 0x801209e <Reset_Handler+14>
0x080120aa <+26>: ldr r2, [pc, #44] ; (0x80120d8 <LoopFillZerobss+34>)
0x080120ac <+28>: ldr r4, [pc, #44] ; (0x80120dc <LoopFillZerobss+38>)
0x080120ae <+30>: movs r3, #0
0x080120b0 <+32>: b.n 0x80120b6 <Reset_Handler+38>
0x080120b2 <+34>: str r3, [r2, #0]
0x080120b4 <+36>: adds r2, #4
0x080120b6 <+38>: cmp r2, r4
0x080120b8 <+40>: bcc.n 0x80120b2 <Reset_Handler+34>
=> 0x080120ba <+42>: bl 0x8000d04 <SystemInit> #带L的跳转指令需要保存返回值,不带的不需要;但这里不需要压栈
0x080120be <+46>: bl 0x800d750 <__libc_init_array>
0x080120c2 <+50>: bl 0x8001488 <main>
0x080120c6 <+54>: bx lr
End of assembler dump.
(gdb) si
SystemInit ()
at system_stm32f4xx.c:371
371 {
(gdb) info registers
r0 0x20000000 536870912
r1 0x20000360 536871776
r2 0x20001764 536876900
r3 0x0 0
r4 0x20001764 536876900
r5 0x0 0
r6 0x0 0
r7 0x0 0
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x0 0
sp 0x20010000 0x20010000
lr 0x80120bf 134291647 #这里的函数调用需要保存返回值
pc 0x8000d04 0x8000d04 <SystemInit>
xPSR 0x61000000 1627389952
fpscr 0x0 0
msp 0x20010000 0x20010000 #这里的函数调用不需要压栈,所以仍是满栈
psp 0x0 0x0
primask 0x0 0
basepri 0x0 0
faultmask 0x0 0
control 0x0 0
Dump of assembler code for function SystemInit:
=> 0x08000d04 <+0>: push {r7} ;将R7中的值压栈,即R7中的值在当前sp指向的地址空间
0x08000d06 <+2>: add r7, sp, #0
0x08000d08 <+4>: ldr r3, [pc, #24] ; (0x8000d24 <SystemInit+32>)
0x08000d0a <+6>: ldr.w r3, [r3, #136] ; 0x88
0x08000d0e <+10>: ldr r2, [pc, #20] ; (0x8000d24 <SystemInit+32>)
0x08000d10 <+12>: orr.w r3, r3, #15728640 ; 0xf00000
0x08000d14 <+16>: str.w r3, [r2, #136] ; 0x88
0x08000d18 <+20>: nop
0x08000d1a <+22>: mov sp, r7
0x08000d1c <+24>: ldr.w r7, [sp], #4 ;将当前sp指向的地址空间处的值取出传给R7,并且sp+=4;这样R7前后值未变,sp回到原始水位
0x08000d20 <+28>: bx lr
0x08000d22 <+30>: nop
0x08000d24 <+32>: ; <UNDEFINED> instruction: 0xed00e000
End of assembler dump.
3:内联汇编语法, 参阅 http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#s6
内敛汇编的基本形式是只有汇编语句,因此如果在汇编代码中更改了一些寄存器并从asm返回而没有恢复这些更改,那么就会发生不好的事情。这是因为GCC不知道寄存器内容的变化,这会给我们带来麻烦,尤其是当编译器进行一些优化时。它将假设某个寄存器包含某个变量的值,我们可能在没有通知GCC的情况下更改了该变量,并且它像什么都没发生一样继续。我们能做的就是要么使用那些没有副作用的指令,要么在我们退出或等待崩溃时修复问题。这就是我们想要一些扩展功能的地方(扩展引入了:输出数:输入数:涉及的寄存器)。在基本的内联汇编中,我们只有指令。在扩展汇编中,我们还可以指定操作数。它允许我们指定输入寄存器、输出寄存器和一个被破坏的寄存器列表。指定被破坏的寄存器列表不是强制性的,我们可以将头痛留给GCC,这可能更适合GCC的优化方案。无论如何,基本格式是
内联汇编中的3个:,如果只需要第一个则后两个:可以省略,如果只需要第2个则可以只忽略第3个,一共保留2个:即可
汇编程序模板由汇编指令组成,如果存在多条指令则除最后一条指令外的其他指令都需要以\n\t结尾。每个操作数由一个操作数约束字符串和括号中的C表达式描述。冒号将汇编程序模板与第一个输出操作数分隔开,另一个冒号将最后一个输出操作数来与第一个输入(如果有的话)分隔开。逗号分隔每组中的操作数。操作数的总数限制为十个或机器描述中任何指令模式中的最大操作数,以较大者为准。 如果没有输出操作数,但有输入操作数,则必须在输出操作数所在的位置周围放置两个连续的冒号。
上面的内联将fill_value计数次数填充到寄存器edi所指向的位置。它还向gcc表示,寄存器eax和edi的内容不再有效(被修改了)。
此例我们使用汇编指令使“b”的值等于“a”的值。
“b”是输出操作数,由%0引用,“a”是输入操作数,被%1引用。因为要使b等于a,即b = a,因此左值b为输出操作数; “r”是对 (输出/输入)操作数的约束。我们稍后将详细了解约束条件。目前,“r”对GCC表示使用任何寄存器来存储操作数,而没有具体指定。输出操作数约束应具有约束修饰符“=”。这个修饰符表示它是输出操作数,并且只能写入。寄存器eax名称前面有两个%。这有助于GCC区分操作数和寄存器。操作数(即%0 和 %1)的前缀只有一个%。 第三个冒号后面的寄存器%eax告诉GCC%eax的值将在“asm”中被修改,所以GCC不会使用这个寄存器来存储任何其他值(因为存了会被修改)。当“asm”的执行完成时,“b”将反映更新后的值,因为它被指定为输出操作数。换句话说,对“asm”内部的“b”所做的更改应该反映在“asm“外部。
汇编程序模板包含插入到C程序中的一组汇编指令。格式如下:每条指令都应该用双引号括起来,或者整组指令都应该放在双引号内。每条指令还应以分隔符结尾。有效的分隔符是换行符(\n)和分号(;)。'\n'后面可以跟一个制表符(\t)。汇编指令中由%0、%1…表示C表达式对应的操作数。
C表达式充当“asm”内部汇编指令的操作数。每个操作数首先作为操作数约束写在双引号中。对于输出操作数,引号中也会有一个约束修饰符,然后跟随代表操作数的C表达式。即“constraint”(C表达式)的形式。对于输出操作数,会有一个额外的修饰符。约束主要用于决定操作数的寻址模式。它们还用于指定要使用的寄存器。如果我们使用多个操作数,它们用逗号分隔。在汇编程序模板中,每个操作数都由数字引用。编号如下。如果总共有n个操作数(包括输入和输出),则第一个输出操作数编号为0,按递增顺序连续,最后一个输入操作数编号n-1。操作数的最大数目正如我们在上一节中看到的那样。
输出操作数表达式必须是左值,输入操作数不受这样的限制。
常用约束:
1:寄存器操作数约束 r
当使用此约束指定操作数时,它们将存储在通用寄存器(GPR)中。以 asm(“movl%%eax,%0\r\n”:“=r”(myval));为例:变量myval保存在一个寄存器中,寄存器eax中的值被复制到该寄存器中,myval的值从该寄存器更新到内存中。当指定了“r”约束时,gcc可以将该变量保留在任何可用的GPR中。要指定寄存器,必须使用特定的寄存器约束直接指定寄存器名称。它们是:
2:内存操作数约束 m
当操作数在内存中时,对它们执行的任何操作都将直接发生在内存位置,而寄存器约束则相反,寄存器约束首先将值存储在要修改的寄存器中,然后将其写回内存位置。但寄存器约束通常只有在指令绝对必要或显著加快进程时才使用。如果C变量需要在“asm”中更新,并且您确实不想使用寄存器来保存其值,那么内存约束可以最有效地使用。例如,idtr的值存储在内存位置loc中:asm(“sidt%0\n”::“m”(loc));
3:匹配(数字)约束
在某些情况下,单个变量可以同时用作输入和输出操作数。这种情况可以通过使用匹配约束在“asm”中指定。asm(“incl%0”:“=a”(var):“0”(var”);我们在操作数小节中也看到了类似的例子。在这个匹配约束的例子中,寄存器%eax被用作输入和输出变量。var输入被读取到%eax,更新后的%eax在递增后再次存储在var中。此处的“0”指定与第0个输出变量相同的约束。也就是说,它指定var的输出实例应仅存储在%eax中。可以使用此约束:在从变量读取输入或修改变量并将修改写回同一变量的情况下。在不需要输入和输出操作数的单独实例的情况下。使用匹配约束最重要的效果是,它们可以有效地使用可用寄存器。
4:使用的其他一些约束包括:
“m”:内存操作数是允许的,具有机器通常支持的任何类型的地址。
“o”:允许内存操作数,但前提是地址可偏移。也就是说,在地址上加一个小的偏移量就可以得到一个有效的地址。
“V”:不可偏移的内存操作数。换句话说,任何符合“m”约束但不符合“o”约束的东西。
“i”:允许立即数整数操作数(具有常数值的操作数)。这包括符号常量,其值只有在汇编时才知道。
“n”:允许使用具有已知数值的立即数整数操作数。许多系统不能为小于一个字宽的操作数支持汇编时间常数。这些操作数的约束应使用“n”而不是“i”。
“g”:允许使用任何寄存器、内存或立即数整数操作数,但非通用寄存器的寄存器除外。
5:以下约束是x86特定的:
“r”:注册操作数约束,查看上面给出的表。
“q”:寄存器a、b、c或d。
“I”:常数,范围为0到31(用于32位移位)。
“J”:常数范围为0到63(用于64位移位)。
“K”:0xff。
“L”:0xffff。
“M”:0、1、2或3(lea指令的移位)。
“N”:常数,范围为0到255(用于out指令)。
“f”:浮点寄存器
“t”:第一个(栈顶)浮点寄存器
“u”:第二个浮点寄存器
“A”:指定“A”或“d”寄存器。这主要适用于64位整数值,该64位整值旨在由保持最高有效位的‘d’寄存器和保持最低有效位的“a”寄存器返回。
在使用约束时,为了更精确地控制约束的效果,GCC为我们提供了约束修饰符。最常用的约束修饰符有
“=”:表示此操作数仅对此指令进行写入;先前的值被丢弃并由输出数据代替。
“&”:表示此操作数是早期clobber操作数,在使用输入操作数完成指令之前会对其进行修改。因此,此操作数可能不在用作输入操作数或任何内存地址的一部分的寄存器中。如果输入操作数仅用作输入发生在写入早期结果之前,则可以将其绑定到早期clobber操作数。
约束条件的列表和解释并不完整。示例可以更好地理解内联asm的使用和用法。在下一节中,我们将看到一些示例,在那里我们将找到更多关于clobber列表和约束的信息。
4:语法
在Intel语法中,基寄存器用'['和']'封装,而在AT&T中,它们变成'('和')'; AT&T中的[ ]表示地址,即[addr]
Intel "mov al, byte ptr foo"
AT&T "movb foo, %al"
MOV 指令码
语法: MOV A B
解释: Inter 汇编语法是把右值传入左值 A <---- B
GUN 汇编语法是把左值传入右值 A ----> B
GUN的汇编是AT&T 格式的;内核寄存器名前有无%用于区分是AT&T格式汇编(有%,mov是从左往右)还是intel格式汇编(无%,mov是从右往左)
4:C语言中对所有标号/变量的操作,都是对其值的操作,而不是操作其地址值,即使是指针变量也是如此;
对指针变量的操作也是指针变量的值,即它指向的变量的地址值,而不是指针变量本身的地址值。
asm语言中对所有标号/变量的操作,都是对其地址的操作,而不是操作其值,即使是普通变量也是如此;
5:以 :结尾的标号(标号即地址值)表示位置计数器的当前值,即此符号的值是 . 的值;
调试ld文件中的 . 可通过PROVIDE(__debug_local_count_value2 = .); 命令,在map中看__debug_local_count_value2 的值
调试Makefile文件中的变量可通过 $(info, ---1--- $(AAA))
6:gun汇编器(as)的伪标号都是相同的含义和作用,不论是gcc下的as, 还是交叉编译下的arm-eabi-gcc下的as,还是hightec下的tricore-gcc下的as;
.section都是描述段,.size都是描述大小,等等... 伪指令不生成机器码,只起引导汇编器工作和方便人的理解的作用
7:gun汇编伪指令的名称都以 . 开始,其余是字符,并且大小写无关;因此名称中带 _ 的必然是标号;标号对应真实的物理地址,伪指令只是给汇编器使用的,起到一个辅助的作用,伪指令可以理解为汇编器能识别的关键字;标号会被翻译为绝对地址,伪指令不会翻译为机器码。
eg:.func 和 .endfunc 命令间的 _funname 和 funname 代表的是同一个函数 funname,.func 和 .endfunc就是一组伪指令。
8:gun汇编中的符号(symbol 即标识符)由字母,数字,_ . 任意拼接构成(部分芯片的汇编还支持$符号),同时数字不能作为开头, (相较与c的标识符多了 . 和 $);标号(label)由符号的结尾追加 : 构成,标号对应真实的物理地址值(. 活动位置计数器的当前值),重复定义同名的标号会以文件中最先出现的位置为准;标号后可以有空格,但 :前决不能有空格(因为 : 属于标号的整体)
9:内联汇编输出表达式 :"=a"(tmp) 表示将eax寄存器的值传入变量tmp中 (或 tmp的值将通过eax传入),即tmp = eax
= 表示(中的表达式)是 只可写的;+ 表示(中的表达式)是 可读可写的;没有则表示(中的表达式)是 只可读的
10:内联汇编中指令部分的%0表示自从第一个:后面出现的第1个(中的表达式);%1表示自从第一个:后面出现的第2个(中的表达式),(中的同名表达式占用同一个%值);
批注:第几个(中的表达式)是指以 第一个:为界,其后所有参数从左到右的排序,而非仅限制于第一个和第二个:间的参数。