ARM杂散知识
画重点:
1.存储器格式:重点是大小端识别 经常考
2.对齐后结构体占用空间大小:使用aligned,packed,#pragma pack()三种方式都要会
Thumb指令集
Thumb指令集能够以16位的系统开销得到32位的系统性能
正常ARM指令PC+4,Thumb指令PC+2
Thumb指令集与ARM指令集的区别
Thumb指令集没有协处理器指令、信号量指令以及访问CPSR或SPSR的指令,没有乘加指令及64位乘法指令等,且指令的第二操作数受到限制;除了跳转指令B有条件执行功能外,其他指令均为无条件执行;大多数Thumb数据处理指令采用2地址格式。Thumb指令集与ARM指令集的区别一般有如下几点:
Ø 跳转指令
程序相对转移,特别是条件跳转与ARM代码下的跳转相比,在范围上有更多的限制,转向子程序是无条件的转移。
Ø 数据处理指令
数据处理指令是对通用寄存器进行操作,在大多数情况下,操作的结果须放入其中一个操作数寄存器中,而不是第三个寄存器中。
数据处理操作比ARM状态的更少,访问寄存器R8—R15受到一定限制。
(除MOV和ADD指令访问寄存器R8—R15外,其他数据处理指令总是更新CPSR中ALU状态标志)
访问寄存器R8—R15的Thumb数据处理指令不能更新CPSR中的ALU状态标志
Ø 单寄存器加载和存储指令
在Thumb状态下,单寄存器加载和存储指令只能访问寄存器R0—R7
Ø 批量寄存器加载和存储指令
LDM和STM指令可以将任何范围为R0——R7的寄存器子集加载或存储
ARM处理器模式
处理器模式 | 描述 |
---|---|
用户模式(User,usr) | 正常程序执行的模式 |
快速中断模式(FIQ,fiq) | 用于高速数据传输和通道处理 |
外部中断模式(IRQ,irq) | 用于通常的中断处理 |
特权模式(Supervisor,sve) | 供操作系统使用的一种保护模式 |
数据访问中止模式(Abort,abt) | 用于虚拟存储及存储保护 |
未定义指令中止模式(Undefined,und) | 用于支持通过软件仿真硬件的协处理器 |
系统模式(System,sys) | 用于运行特权级的操作系统任务 |
除了用户模式以外的其他6种处理器模式称为特权模式(Privileged Modes)。除系统模式外,其他模式又称为异常模式。
处理器模式可以通过软件切换,也可以通过外部终端或异常处理过程进行切换,大多数的用户程序运行在用户模式下(这时用户程序不能访问一些受操作系统保护的系统资源,应用程序也不能直接进行处理器模式的切换)。当需要进行处理器模式的切换时,应用程序可以产生异常处理,在异常处理过程中进行处理器模式的切换。
每一种异常模式都有一组寄存器,供相应的异常处理程序使用,以保证在进入异常模式时,用户模式下的寄存器不会被破坏。
系统模式并不是通过异常模式进入的,它和用户模式具有完全一样的寄存器,但是系统模式属于特权模式,可以访问所有的系统资源,也可以直接进行处理器模式切换。
ARM寄存器
ARM处理器共有37个寄存器,包括:
- 31个通用寄存器,包括程序计数器(PC)在内,这些寄存器都是32位寄存器。
- 6个状态寄存器,这些寄存器都是32位寄存器,但目前只使用了其中的12位。
对于ARM处理器的7种不同的处理器模式,每一种处理器模式下都有一组相应的寄存器组。任意时刻,可见的寄存器包括15个通用寄存器(R0-R14)、一个或两个状态寄存器及程序计数器(PC)。在所有的寄存器中,有些是各模式通用的同一个物理寄存器,有些是各模式自己拥有的独立的物理寄存器。
更详细的描述可见http://blog.chinaunix.net/uid-20443992-id-5700979.html
ARM体系的异常中断
异常中断种类:
异常中断名称 | 含义 |
---|---|
复位(Reset) | 复位异常通常用在下面几种情况下: 系统上电时 系统复位时 跳转到复位中断处向量执行,称为软复位 |
未定义的指令(undefined instruction) | 当ARM处理器或者系统中的协处理器认为当前指令未定义时, 产生未定义的指令异常中断,可以通过该异常中断机制仿真浮点向量运算 |
软件中断(software interrupt SWI) | 由用户定义的中断指令,可用于用户模式下的程序调用特权操作指令, 在实时操作系统钟可以通过该机制实现系统调用功能 |
指令预取中止(Prefetch Abort) | 处理器预取的指令地址不存在,或者该地址不允许当前指令访问, 处理器产生指令预取中止异常中断 |
数据访问中止(Data Abort) | 处理器数据访问指令的目标地址不存在,或者该地址不允许当前指令访问, 处理器产生数据访问中止异常中断 |
外部中断请求(IRQ) | 处理器的外部中断请求引脚有效,且CPSR寄存器的I控制位被清除时产生, 系统中各种外设通常通过这种方式产生中断请求处理器服务 |
快速中断请求(FIQ) | 当处理器的外部快速中断请求引脚有效,且CPSR寄存器的F控制位被清除时产生 |
ARM体系中存储系统
地址空间
ARM体系使用单一的平板地址空间,该地址空间大小为232个8位字节,这些字节单元的地址是一个无符号的32位数值,其取值范围为0到232-1。
ARM的地址空间也可以看作是230个32位的字单元,这些字单元的地址可以被4整除,即该地址的低两位为0b00,地址为A的字数据包括地址为A,A+1,A+2,A+3这4个字节单元的内容
存储器格式
ARM体系中,每个字单元包含4个字节单元或者两个半字单元。
在字单元中,4个字节哪一个是高位字节,哪一个是低位字节则有两种不同的格式:big-endian和little-endian格式
在big-endian格式中,对于地址为A的字单元包括字节单元A、A+1、A+2、A+3,其中字节单元从高位到低位字节顺序为:A、A+1、A+2、A+3,其格式大概为:
31-24bit | 23-16bit | 15-8bit | 7-0bit |
---|---|---|---|
A | A+1 | A+2 | A+3 |
在little-endian格式中,对于地址为A的字单元包括字节单元A、A+1、A+2、A+3,其中字节单元从高位到低位字节顺序为:A+3、A+2、A+1、A,其格式大概为:
31-24bit | 23-16bit | 15-8bit | 7-0bit |
---|---|---|---|
A+3 | A+2 | A+1 | A |
关于大小端的判断:
bool isBig()
{
int a = 1;
char *p = (char*)&a;
if(*p == 1)
{
return false;//小端
}
else
{
return true;//大端
}
}
int main()
{
if(!isBig())
{
printf("Is little\n");
}
else
{
printf("Is big\n");
}
return 0;
}
非对齐的存储访问操作
非对齐的指令预取操作
无论处理器处于ARM还是Thumb状态,如果写入到寄存器PC的值是非对齐的(ARM下低两位不为0,Thumb下最低位不为0)则要么指令执行的结果不可预知,要么地址值中后面的2位或1位被忽略。
非对齐的数据访问操作
对于Load/Store操作,如果是非对齐的数据访问操作,系统定义了下面3种可能的结果:
- 执行的结果不可预知
- 忽略字单元地址的低两位(即访问地址为 destaddr & 0XFFFFFFFC 的字单元);忽略半字单元地址的最低位(即访问地址为 destaddr & 0XFFFFFFFE 的半字单元)
- 由存储系统进行忽略,比如寻址0X20000001的地址,该值被原封不动地传进存储系统,和第二条类似
什么情况容易发生非对齐访问
出现alignment fault问题,通常是用户编写的代码导致。估计很多程序猿在编写代码(特别是c/c++代码)时,从未考虑过这样的问题,那是因为多数可能都在X86架构下的进行代码开发,而且没有考虑过代码的移植性,如前面所说X86硬件会自动处理非对齐问题,用户感知不到,但这种情况下,由此带来的性能损耗,用户可能也关注不到了。另一方面,部分情况下,编译器也会自动做padding处理(如对结构体的自动填充对齐),这也进一步让程序猿们减少了对alignment fault的关注。
最常见的可能导致alignment fault的代码编写方式如:
1) 指针转换
将低位宽类型的指针转换为高位宽类型的指针,如:将char * 转为int *,或将void *转为结构体指针。这类操作是导致alignment fault的最主要的来源,在分析定位问题时,需要特别关注。对于出现异常却又必须这样使用的场景,对这类转换后的指针进行访问时,如果不能确认其对应的地址是对齐的,则应该使用memcpy访问(memcpy方式不存在对齐问题)。另外,建议转换后立即使用,不要将其传递到其他函数和模块,防止扩展,带来潜在的问题。
2) 使用packed属性或者编译选项(笔试面试经常会问到的问题)
这样的操作会关闭编译器的自动填充功能,从而使结构体中各个字段紧凑排列,如果排列时未处理好对齐,则可能导致alignment fault。一些场景下(内核中也较常见)确实需要用户自行紧凑排列结构体,可节省空间(在内存资源稀缺的场景下,很有用),此时需要特别关注对齐问题,建议通过填充的方法尽量对齐,如此可能会导致空间浪费,但是会提升访问性能,典型的“以空间换时间”的思路。如果对空间有强烈要求,而可以接受性能损失,也可以不考虑对齐,不做padding,但在访问这些结构体的数据时,需要全部使用memcpy的方式。
用__attribute__进行对齐(aligned和packed)
比如如下代码:
struct stu{
char sex;
int length; ;
char name[2];
char value[15];
}__attribute__ ((aligned (1)));
或者是如下代码:
typedef struct {
/* frame control format. */
union {
uint16_t fcf;
struct {
uint16_t type : 3;
uint16_t security : 1;
uint16_t frame_pending : 1;
uint16_t ar : 1;
uint16_t panid_compression : 1;
uint16_t reserved : 3;
uint16_t dest_addr_mode : 2;
uint16_t frame_version : 2;
uint16_t src_addr_mode : 2;
} __attribute__ ((packed)) fcf_s;
} __attribute__ ((packed)) fcf_u;
/* sequence number. */
uint8_t seq_num;
} __attribute__ ((packed)) mhr_t;
可见 常用的方法有__attribute__((aligned(n)))和__attribute__((packed))两种:
aligned方法
这种方法可能更常用也更常考一些,多用于进行对齐操作,使用方法为:
aligned后接要对齐的字节数标准n,当结构体内成员占用的最大字节数不超过n时,按照这个数n进行对齐,当超过n时按照结构体内成员占用的最大字节数进行对齐。例如上述代码中对齐标准为1,但结构体中的最大不可拆分成员变量为int,其占用字节数为4,1<4,因此按照4字节方式进行对齐,对齐结果如下图所示,每行为一个字(32bit 4字节),由于对齐标准为4,sex和length就不能放在同一字中,空白的部分被reserved填充(由编译器进行操作)。由此也能推得,当n<=4时对齐后的内存排布都是下图方式,占用内存为28
当n=8时,由于4<8,因此按照8字节方式进行对齐,对齐结果如下图:其中每行有8个字节,最终占用内存为32
这时候产生了一个疑问:为什么sex和lenghth、name不能放到同一个8字节内存中呢?
这是因为:每个成员所在内存的地址必须为其大小的整数倍,即length的大小为4,如果紧接着sex排放其内存地址将是……1,不满足4的整数倍。
packed方法
packed方法与编译器联系更密切,对于不同的编译器来说得到的结果可能不一样,如下的代码,我在codeblocks上编译和在linux下用gcc编译得到的大小是不同的,在gcc下编译时返回结果为5,证明对其进行了紧凑型编译,而在codeblocks中编译运行结果仍为8,(不知道是不是编译器环境设置错误),不过可以确定的是,packed方法可以让编译器不按照对齐规则进行编译,而是使内部变量连续地排布,这对一些跨平台的移植很有帮助,比如各种跨平台的协议栈等。
struct stu{
char sex;
int length;
}__attribute__((packed)) ;
“:”符号的引入(位域操作)
如下面代码块中就是IEEE802.15.4协议中帧头部的定义,由于需要严格按照下图中的规范中各位进行解析,所以必须按照特定的位数进行对齐,而在一个变量中对其进行定义显然有些麻烦,因此位域操作使得代码可以很明朗地对这类结构进行描述。
位域操作的用法是:在变量中,只取变量最低几位,并将其命名为这个变量名,而这个位数取决于":"后面的数字。比如下面的表格和代码中的各位是一一对应的,使用时也可以在一个结构体中直接通过变量名对某一位进行读写,这样做还不会浪费额外的空间。
typedef struct {
/* frame control format. */
union {
uint16_t fcf;
struct {
uint16_t type : 3;
uint16_t security : 1;
uint16_t frame_pending : 1;
uint16_t ar : 1;
uint16_t panid_compression : 1;
uint16_t reserved : 3;
uint16_t dest_addr_mode : 2;
uint16_t frame_version : 2;
uint16_t src_addr_mode : 2;
} __attribute__ ((packed)) fcf_s;
} __attribute__ ((packed)) fcf_u;
/* sequence number. */
uint8_t seq_num;
} __attribute__ ((packed)) mhr_t;
####### 位域的存储
C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间。
位域的具体存储规则如下:
-
当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
-
当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会。
-
如果成员之间穿插着非位域成员,那么不会进行压缩。
无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。
编译选项对齐
#pragma pack (n) // 作用:C编译器将按照n个字节对齐。
#pragma pack () // 作用:取消自定义字节对齐方式。
#pragma pack (push,1) // 作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐
#pragma pack(pop) // 作用:恢复对齐状态