• 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-endianlittle-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
    ");
    	}
    	else
    	{
    		printf("Is big
    ");
    	}
    	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

    aKHR0A.png

    ​ 当n=8时,由于4<8,因此按照8字节方式进行对齐,对齐结果如下图:其中每行有8个字节,最终占用内存为32

    aKHL0s.png

    这时候产生了一个疑问:为什么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协议中帧头部的定义,由于需要严格按照下图中的规范中各位进行解析,所以必须按照特定的位数进行对齐,而在一个变量中对其进行定义显然有些麻烦,因此位域操作使得代码可以很明朗地对这类结构进行描述。

    ​ 位域操作的用法是:在变量中,只取变量最低几位,并将其命名为这个变量名,而这个位数取决于":"后面的数字。比如下面的表格和代码中的各位是一一对应的,使用时也可以在一个结构体中直接通过变量名对某一位进行读写,这样做还不会浪费额外的空间。

    aK7zsH.png

    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语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间。

    位域的具体存储规则如下:

    1. 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。

    2. 当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会。

    3. 如果成员之间穿插着非位域成员,那么不会进行压缩。

    无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。

    编译选项对齐
    #pragma pack (n)           	 // 作用:C编译器将按照n个字节对齐。
    #pragma pack ()              // 作用:取消自定义字节对齐方式。
    #pragma pack (push,1)     	 // 作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐
    #pragma pack(pop)            // 作用:恢复对齐状态
    
  • 相关阅读:
    ACM-ICPC 2018 徐州赛区网络预赛 G题
    ACM-ICPC 2018 沈阳赛区网络预赛 K题
    ACM-ICPC 2018 沈阳赛区网络预赛 K题
    数据结构实验之栈与队列七:出栈序列判定
    数据结构实验之栈与队列七:出栈序列判定
    Python isspace()方法
    Python isnumeric()方法
    Python islower()方法
    Python isdigit()方法
    Python isdecimal()方法
  • 原文地址:https://www.cnblogs.com/zhaipanger/p/13406387.html
Copyright © 2020-2023  润新知