(1)SVC指令:
摘自 http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0203ic/Cacdfeci.html
与更早版本的 ARM 处理器一样,有一条 SVC 指令可生成 SVC。 SVC 通常用于在操作系统上请求特权操作或访问系统资源。
SVC 指令中嵌入了一个数字,这个数字通常称为 SVC 编号。 在大多数 ARM 处理器上,此编号用于指示要请求的服务。在微控制器架构上,处理器在最初进入异常时,将参数寄存器保存到堆栈中。
在 SVC 处理程序执行第一个指令之前较晚处理的异常可能会损坏 R0 到 R3 中仍保存的参数副本。 这表示参数的堆栈副本必须由 SVC 处理程序使用。 还必须通过修改堆栈中的寄存器值将任何返回值传递回调用方。 为此,必须在 SVC 处理程序的开头实现一小段汇编代码。这样可以确定寄存器的保存位置,从指令中提取 SVC 编号,并将编号和参数指针传递到用 C 编写的处理程序主体中。
Example 6.21 是一个 SVC 处理程序示例。 这段代码测试处理器所设置的 EXC_RETURN 值,确定在调用 SVC 时,使用的是哪个堆栈指针。这对于重入 SVC 可能很有用,但在大多数系统上是不必要的,因为在典型系统设计中,只会从使用进程堆栈的用户代码中调用 SVC。在这种情况下,汇编代码可以包含单条 MSR 指令,后跟指向处理程序 C 程序体的尾调用跳转(B 指令)。
Example 6.21. SVC 处理程序示例
__asm void SVCHandler(void)
{
IMPORT SVCHandler_main
TST lr, #4
ITE EQ
MRSEQ R0, MSP
MRSNE R0, PSP
B SVCHandler_main
}
void SVCHandler_main(unsigned int * svc_args)
{
unsigned int svc_number;
/*
* Stack contains:
* R0, R1, R2, R3, R12, R14, the return address and xPSR
* First argument (R0) is svc_args[0]
*/
svc_number = ((char *)svc_args[6])[-2];
switch(svc_number)
{
case SVC_00:
/* Handle SVC 00 */
break;
case SVC_01:
/* Handle SVC 01 */
break;
default:
/* Unknown SVC */
break;
}
}
Example 6.22 演示如何为很多 SVC 进行不同声明。 __svc 是编译器关键字,该关键字将函数调用替换为包含指定编号的 SVC 指令。
Example 6.22. 在 C 代码中调用 SVC 的示例
#define SVC_00 0x00
#define SVC_01 0x01
void __svc(SVC_00) svc_zero(const char *string);
void __svc(SVC_01) svc_one(const char *string);
int call_system_func(void)
{
svc_zero("String to pass to SVC handler zero");
svc_one("String to pass to a different OS function");
SVC 处理程序
发生异常时,异常处理程序可能需要确定处理器是在 ARM 状态还是在 Thumb 状态。
特别是 SVC 处理程序,更需要读取处理器状态。 通过检查 SPSR 的 T 位可确定处理器状态。 该位在 Thumb 状态时为 1,在 ARM 状态时为 0。
ARM 和 Thumb 指令集均有 SVC 指令。 在 Thumb 状态下调用 SVC 时,必须考虑以下情况:
指令的地址在 lr–2,而不在 lr–4。
该指令本身为 16 位,因而需要半字加载,请参阅Figure 6.3。
在 ARM 状态下,SVC 编号以 8 位存储,而不是 24 位。
Figure 6.3. Thumb SVC 指令
Example 6.8 演示处理 SVC 异常的 ARM 代码。 通过动态调用 SVC,可以增大 Thumb 状态下可访问的 SVC 编号的范围。
Example 6.8. SVC 处理程序
PRESERVE8
AREA SVC_Area, CODE, READONLY
EXPORT SVC_Handler IMPORT C_SVC_Handler
T_bit EQU 0x20 ; Thumb bit (5) of CPSR/SPSR.
SVC_Handler
STMFD sp!, {r0-r3, r12, lr} ; Store registers
MOV r1, sp ; Set pointer to parameters
MRS r0, spsr ; Get spsr
STMFD sp!, {r0, r3} ; Store spsr onto stack and another
; register to maintain 8-byte-aligned stack
TST r0, #T_bit ; Occurred in Thumb state?
LDRNEH r0, [lr,#-2] ; Yes: Load halfword and...
BICNE r0, r0, #0xFF00 ; ...extract comment field
LDREQ r0, [lr,#-4] ; No: Load word and...
BICEQ r0, r0, #0xFF000000 ; ...extract comment field
; r0 now contains SVC number
; r1 now contains pointer to stacked registers
BL C_SVC_Handler ; Call main part of handler
LDMFD sp!, {r0, r3} ; Get spsr from stack
MSR SPSR_cxsf, r0 ; Restore spsr
LDMFD sp!, {r0-r3, r12, pc}^ ; Restore registers and return
END
确定要调用的 SVC
进入 SVC 处理程序后,必须确定要调用哪个 SVC。 此信息可存储在指令本身的 0-23 位,如Figure 6.4 所示,或将其传递给某个整数寄存器,通常为 R0-R3 中的一个。
Figure 6.4. ARM SVC 指令
顶层 SVC 处理程序可以相对于 LR 加载 SVC 指令 请用汇编语言、C/C++ 内联或嵌入式汇编器编写。
处理程序必须首先将导致异常的 SVC 指令加载到寄存器中。 此时,SVC LR 保存 SVC 指令的下一个指令的地址,这样 SVC 加载到了寄存器(本例中为 R0)中,代码如下:
LDR R0, [lr,#-4]
然后,处理程序可检查注释字段位,以决定所需的操作。 通过清除操作码的前八位来提取 SVC 编号:
BIC R0, R0, #0xFF000000
Example 6.9 演示如何用这些指令编写顶层 SVC 处理程序。 有关在 ARM 状态和 Thumb 状态下处理 SVC 指令的处理程序示例,请参阅Example 6.8。
Example 6.9. 顶层 SVC 处理程序
PRESERVE8
AREA TopLevelSVC, CODE, READONLY ; Name this block of code.
EXPORT SVC_Handler
SVC_Handler
PUSH {R0-R12,lr} ; Store registers.
LDR R0,[lr,#-4] ; Calculate address of SVC instruction
; and load it into R0.
BIC R0,R0,#0xFF000000 ; Mask off top 8 bits of instruction
; to give SVC number.
;
; Use value in R0 to determine which SVC routine to execute.
;
LDM sp!, {R0-R12,pc}^ ; Restore registers and return.
END
汇编语言编写的 SVC 处理程序
要调用请求的 SVC 编号的处理程序,最简单的方法是使用跳转表。 如果 R0 包含 SVC 编号,则Example 6.10 中的代码可以插入到Example 6.9 提供的顶层处理程序中,插入位置在 BIC 指令之后。
Example 6.10. SVC 跳转表
AREA SVC_Area, CODE, READONLY
PRESERVE8
IMPORT SVCOutOfRange
IMPORT MaxSVC
CMP R0,#MaxSVC ; Range check
LDRLS pc, [pc,R0,LSL #2]
B SVCOutOfRange
SVCJumpTable
DCD SVCnum0
DCD SVCnum1
; DCD for each of other SVC routines
SVCnum0 ; SVC number 0 code
B EndofSVC
SVCnum1 ; SVC number 1 code
B EndofSVC
; Rest of SVC handling code
EndofSVC
; Return execution to top level
; SVC handler so as to restore
; registers and return to program.
END
C 语言和汇编语言编写的 SVC 处理程序
尽管顶层处理程序始终必须用 ARM 汇编语言编写,处理每个 SVC 的例程既可用汇编语言编写,也可用 C 语言编写。有关限制的说明,请参阅在超级用户模式下使用 SVC。
顶层处理程序使用 BL 指令可跳转到相应的 C 函数。 因为 SVC 编号是由汇编例程加载到 R0 中的,所以作为第一个参数传递给 C 函数。 举例来说,函数可以将此参数值用在 switch() 语句中,请参阅Example 6.11。
若要调用此 C 函数,可以向Example 6.9 中 SVC_Handler 例程添加下面的代码行:
BL C_SVC_Handler ; Call C routine to handle the SVC
Example 6.11. C 函数中的 SVC 处理程序
void C_SVC_handler (unsigned number)
{
switch (number)
{
case 0 : /* SVC number 0 code */
...
break;
case 1 : /* SVC number 1 code */
...
break;
...
default : /* Unknown SVC - report error */
}
}
超级用户模式堆栈空间可能是有限的,因此要避免使用需要大量堆栈空间的函数。
MOV R1, sp ; Second parameter to C routine...
; ...is pointer to register values.
BL C_SVC_Handler ; Call C routine to handle the SVC.
用 C 语言编写的 SVC 处理程序可以传入和传出值,前提是顶层处理程序将堆栈指针值作为第二个参数(在 R1 中)传递给 C 函数,并且更新 C 函数以访问该值:
void C_SVC_handler(unsigned number, unsigned *reg)
现在,C 函数在主应用程序代码中遇到 SVC 指令时就可访问存储在寄存器中的值了,请参阅 Figure 6.5。 它可从其中读取:
value_in_reg_0 = reg [0];
value_in_reg_1 = reg [1];
value_in_reg_2 = reg [2];
value_in_reg_3 = reg [3];
也可向其中回写:
reg [0] = updated_value_0;
reg [1] = updated_value_1;
reg [2] = updated_value_2;
reg [3] = updated_value_3;
这使已更新的值写入到堆栈的相应位置,然后通过顶层处理程序恢复到寄存器中。
Figure 6.5. 访问超级用户模式堆栈
在超级用户模式下使用 SVC
执行 SVC 指令时:
处理器进入超级用户模式。
CPSR 存储到 SVC SPSR 中。
返回地址存储在 SVC LR 中,请参阅处理器对异常的响应。
如果处理器已经处在超级用户模式下,则会损坏 SVC LR 和 SPSR。
如果在超级用户模式下调用 SVC,则必须存储 SVC LR 和 SPSR,以确保 LR 和 SPSR 的原始值不会丢失。例如,如果某个特定 SVC 编号的处理程序例程调用另一个 SVC,则必须确保该处理程序例程将 SVC LR 和 SPSR 都存储在堆栈中。 这可确保处理程序的每一次调用都保存返回到调用它的 SVC 后面的指令所需要的信息。Example 6.12 演示如何实现这一点。
Example 6.12. SVC 处理程序
AREA SVC_Area, CODE, READONLY
PRESERVE8
EXPORT SVC_Handler
IMPORT C_SVC_Handler
T_bit EQU 0x20
SVC_Handler
PUSH {R0-R3,R12,lr} ; Store registers.
MOV R1, sp ; Set pointer to parameters.
MRS R0, SPSR ; Get SPSR.
PUSH {R0,R3} ; Store SPSR onto stack and another register to maintain
; 8-byte-aligned stack. Only required for nested SVCs.
TST R0,#0x20 ; Occurred in Thumb state?
LDRHNE R0,[lr,#-2] ; Yes: load halfword and...
BICNE R0,R0,#0xFF00 ; ...extract comment field.
LDREQ R0,[lr,#-4] ; No: load word and...
BICEQ R0,R0,#0xFF000000 ; ...extract comment field.
; R0 now contains SVC number
; R1 now contains pointer to stacked registers
BL C_SVC_Handler ; Call C routine to handle the SVC.
POP {R0,R3} ; Get SPSR from stack.
MSR SPSR_cf, R0 ; Restore SPSR.
LDM sp!, {R0-R3,R12,pc}^ ; Restore registers and return.
END
用 C 和 C++ 编写的嵌套 SVC
可用 C 或 C++ 语言编写嵌套的 SVC。 由 ARM 编译器生成的代码根据需要存储和重新加载 lr_SVC。
从应用程序调用 SVC
可用汇编语言或 C/C++ 调用 SVC。
用汇编语言设置所有必需的寄存器值并发出相关的 SVC。 例如:
MOV R0, #65 ; load R0 with the value 65
SVC 0x0 ; Call SVC 0x0 with parameter value in R0
几乎像所有的 ARM 指令那样,SVC 指令可被有条件地执行。
在 C/C++ 中,将 SVC 声明为一个 __SVC 函数并调用它。 例如:
__svc(0) void my_svc(int);
.
.
.
my_svc(65);
这确保了 SVC 以内联方式进行编译,无需额外的调用开销,其前提是:
所有参数都只传入 R0-R3
所有结果都只返回到 R0-R3 中。
参数被传递给 SVC,如同 SVC 是一个真正的函数调用。 但是,如果有二到四个返回值,则必须告诉编译器返回值在一个结构中,并使用 __value_in_regs 命令。这是因为基于 struct 值的函数通常被视为一个 void 函数,它的第一个参数必须是存放结果结构的地址。
Example 6.13 和 Example 6.14 演示一个 SVC 处理程序,该处理程序提供 SVC 编号 0x0、0x1、0x2 和 0x3。 SVC 0x0 和 SVC 0x1 都采用两个整数参数并返回一个结果。SVC 0x2 采用四个参数并返回一个结果。SVC 0x3 采用四个参数并返回四个结果。 此示例位于示例目录(...\svc\main.c 和 ...\svc\svc.h)中。
Example 6.13. main.c
#include
#include "svc.h"
unsigned *svc_vec = (unsigned *)0x08;
extern void SVC_Handler(void);
int main( void )
{
int result1, result2;
struct four_results res_3;
Install_Handler( (unsigned) SVC_Handler, svc_vec );
printf("result1 = multiply_two(2,4) = %d\n", result1 = multiply_two(2,4));
printf("result2 = multiply_two(3,6) = %d\n", result2 = multiply_two(3,6));
printf("add_two( result1, result2 ) = %d\n", add_two( result1, result2 ));
printf("add_multiply_two(2,4,3,6) = %d\n", add_multiply_two(2,4,3,6));
res_3 = many_operations( 12, 4, 3, 1 );
printf("res_3.a = %d\n", res_3.a );
printf("res_3.b = %d\n", res_3.b );
printf("res_3.c = %d\n", res_3.c );
printf("res_3.d = %d\n", res_3.d );
return 0;
}
Example 6.14. svc.h
__svc(0) int multiply_two(int, int);
__svc(1) int add_two(int, int);
__svc(2) int add_multiply_two(int, int, int, int);
struct four_results
{
int a;
int b;
int c;
int d;
};
__svc(3) __value_in_regs struct four_results
many_operations(int, int, int, int);
从应用程序动态调用 SVC
在某些情况下,需要调用直到运行时才会知道其编号的 SVC。 例如,当有很多相关操作可对同一目标执行,并且每个操作都有自己的 SVC 时,就会发生这种情况。 在这种情况下,前几节中介绍的方法不适用。
对此,有几种解决方法,例如:
通过 SVC 编号构建 SVC 指令,将它存储在某处,然后再执行该指令。
使用通用的 SVC(作为一个额外的参数)将一个代码作为对其参数执行的实际操作。 通用 SVC 对该操作进行解码并予以执行。
第二种机制可使用汇编语言将所需的操作数传递到寄存器(通常为 R0 或 R12)中来实现。然后可重新编写 SVC 处理程序,对相应寄存器中的值进行处理。
因为有些值必须用注释字段传递给 SVC,所以有可能将这两种方法结合起来使用。
例如,操作系统可能会只用一条 SVC 指令和一个寄存器来传递所需的操作数。 这使得其他 SVC 空间可用于应用程序特定的 SVC。 在一个特定的应用程序中,如果从指令提取操作数的开销太大,则可使用这个方法。 ARM 和 Thumb 半主机指令就是这样实现的。
Example 6.15 演示如何使用 __svc 将 C 函数调用映射到半主机调用。 该示例是根据示例目录中的 retarget.c 编写的,该文件路径为 ...\emb_sw_dev\source\retarget.c。
Example 6.15. 将 C 函数映射到半主机调用
#ifdef __thumb
/* Thumb Semihosting */
#define SemiSVC 0xAB
#else
/* ARM Semihosting */
#define SemiSVC 0x123456
#endif
/* Semihosting call to write a character */
__svc(SemiSVC) void Semihosting(unsigned op, char *c);
#define WriteC(c) Semihosting (0x3,c)
void write_a_character(int ch)
{
char tempch = ch;
WriteC( &tempch );
}
编译器含有一个机制,支持使用 R12 来传递所需运算的值。 在 AAPCS 下,R12 为 ip 寄存器,并且专用于函数调用。其他时间内可将其用作暂存寄存器。 通用 SVC 的参数被传递到 R0-R3 寄存器中,如前面所述,还可以选择在 R0-R3 中返回值,请参阅 从应用程序调用 SVC。 在 R12 中传递的操作数可以是通用 SVC 调用的 SVC 的编号。 但这不是必需的。
Example 6.16 演示使用通用或间接 SVC 的 C 程序段。
Example 6.16. 使用间接 SVC
__svc_indirect(0x80)
unsigned SVC_ManipulateObject(unsigned operationNumber,
unsigned object,unsigned parameter);
unsigned DoSelectedManipulation(unsigned object,
unsigned parameter, unsigned operation)
{ return SVC_ManipulateObject(operation, object, parameter);
}
它生成以下代码:
DoSelectedManipulation
PUSH {R4,lr}
MOV R12,R2
SVC #0x80
POP {R4,pc}
END
还可使用 __svc 机制从 C 中传递 R0 中的 SVC 编号。 例如,如果将 SVC 0x0 用作通用 SVC,操作 0 为字符读,操作 1 为字符写,则可以进行如下设置:
__svc (0) char __ReadCharacter (unsigned op);
__svc (0) void __WriteCharacter (unsigned op, char c);
可通过如下定义使其具有更好的可读性风格:
#define ReadCharacter () __ReadCharacter (0);
#define WriteCharacter (c) __WriteCharacter (1, c);
但是,如果以这种方式使用 R0,则仅有三个寄存器可用于向 SVC 传递参数。通常,在不得不将除 R0-R3 之外的更多参数传递给子例程时,可通过使用堆栈来完成但是,SVC 处理程序不容易访问堆栈参数,因为这些参数通常在用户模式堆栈中,而不是在 SVC 处理程序使用的超级用户模式堆栈中。