嵌入式Linux C语言程序设计一、二
嵌入式Linux C语言程序设计
1.1 嵌入式Linux下C语言概述
在嵌入式系统中,应用程序的主体是在宿主机中开发完成的,就嵌入式linux而言,此过程则一般在安装有linux的宿主机中完成的。
1.1.1C语言简史
C语言于20世纪70年代诞生于美国的贝尔实验室。
1.1.2C语言的特点
- C语言是结构化的语言
- C语言是模块化的语言
- 程序可移植性好
- C语言运算符丰富、代码效率高
1.1.3嵌入式LinuxC语言编程环境
- vim,vscode
- 编译链接器 GCC
- 调试器GDB
- 项目管理工具makefile
数据
ANSI C 与GNU C
2.1.1ANSI C
1983年,美国国家标准协会(ANSI)根据C语言问世以来各种版本对C的发展和扩充制定了新的标准,并于1989年颁布,被称为ANSI C或C89,C99。
2.1.2 GUN C
GNU项目始创于1984年,旨在开发一个类似UNIX,且为自由软件的完整的操作系统。GCC是GNU的一个项目GCC支持扩展的C称为GNU C
数据类型
- 基本类型
- 构造类型
- 空类型
数据类型 | 标识符 | 数据类型 | 标识符 |
---|---|---|---|
整型 | int | 结构体 | struct |
字符型 | char | 共同体 | union |
浮点型 | float单精度 | 空类型 | void |
浮点型 | double双精度 | 数据类型 | 无 |
枚举型 | enum | 指针类型 | 无 |
2.2基本数据类型
2.2.1整型
跟据类型占用比特数大小依次是,long大小会随CPU处理数据位宽变化而变化,如x86 32位,amd64 64位
short int
int
long int
还分有符号无符号,无符号就是只有正数。
unsigned short int
unsigned int
unsigned long int
赋值
int a = 100;//十进制
int b = 0100;//八进制
int c = 0x100;//十六进制
unsigned int d = 100U;//无符号
long e = 100L;//长整型
unsigned long f = 100UL;//无符号长整型
在嵌入式开发中,经常需要考虑的一点是可移植性的问题,通常,字符是否为有符号数会带来两难的境地,因此最佳方案就是把存储于int型变量的值限制在signed int 和 unsigned int 的交集中,这可以获得最大程度上的可移植性,同时又不牺牲效率。
2.2.2实型
跟据类型占用比特数大小依次是
float
double
long double
字符 | 小数部分 | 指数 |
---|---|---|
+ | 12345678 | 128 |
具体看IEEE754
赋值
float a = 3.0e-2;//0.030000
float b = 0.03;
2.2.3字符型
char c = 'a';//一般占用一个字节8bit
unsigned char b = 'C';
2.2.4枚举
枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型
// /mm/sheme.c
enum sgp_type
{SGP_QUICK,//值是1;SGP_READ,SGP_CHCHE,SGP_WRITE,
}
//可以这样
enum sgp_type
{SGP_QUICK = 2,//值是2;SGP_READ,//值是3;SGP_CHCHE,SGP_WRITE,
}
//也可这样
enum sgp_type
{SGP_QUICK = 2,//值是2;SGP_READ =4,SGP_CHCHE =6,SGP_WRITE =8,
}
2.2.5指针
- 指针的概念:指针是指向内存单元的地址。
- 指针常量 NULL
- 字符串常量 如:”China“
int a = 12;
int *b = &a;
char *c ="China";
2.3变量与常量
2.3.1变量定义
//定义如下没有赋值的。
int a;
unsigned int b;
float c;
double d;
2.3.2 typedef
typedef是C语言的关键字,其作用是为一个数据类型定义一个新名字。如数据基本类型和自定义的数据类型(struct)
typedef 数据类型 自定义数据类型
typedef unsigned long uint32
uint32 a;
unsigned long a;
在大型程序开发中,typedef应用广泛目的1给变量一个易记且意义明确的的新名字,二是简化一些比较复的类型说明,三加上条件编译在不改类型名称的情况下改变变量类型。
2.3.3常量
- const定义常量,不能修改的值。
int const a = 10;//定义赋值一步完成
- #define 宏定义符号常量,预处理后变将代码中的SUM替换成99
#define SUM 99
2.3.4作用域
变量的作用域定义:程序中可以访问一个指示符的一个或多个区域,即变量出现的有效区域,决定了程序的哪些部分通过变量名来访问变量。一个变量根据其作用域的范围可以分为函数原型作用域,局部变量和全局变量。
- 函数原型作用域
//作用域始于"("止于")"
double Area(double radius)
- 局部变量
在函数内部定义的变量称为局部变量。局部变量仅能被定义该变量的模块内部的语句所访问。换言之,局部变量在自己的代码模块之外是不可见。
模块以左花括号开始,以右花括号结束
- 全局变量
与局部变量不同,全局变量贯穿整个程序,它的作用域为源文件,可被源文件的任何一个函数使用。它们在整个程序执行期间保持有效。
- 全局变量的定义和全局变量的声明并不是一回事,全局变量定义必须在所有函数之外,且只能定义一次
[extern] int a,b
- 外部变量可加强函数模块之间的数据联系,但是又使函数要依赖这些变量,因而使得函数的独立性降低。从模块化程序设计的观点来看这是不利的,因此在不必要时尽量不要使用全局变量。
- 全局变量的内存分配是在编译过程中完成的,它在程序全部执行过程中都要占用存储空间,而不是仅在需要时才开辟存储空间
- 在同一源文件中,允许全局变量和局部变量同名。在局部变量的作用域内,全局变量不起作用。因此若在该函数中相要使用全局变量,则不能再定义一个同名的局部变量。
- 全局变量的作用域可以通过关键字extern扩展到整个文件或其他文件。
2.3.5存储模型
变量是程序中数据的存储空间的抽象。变量的存储方式分为静态存储和动态存储两种。
静态存储变量通常是在程序编译时就分配一定的存储空间并一直保持不变,直至整个程序结束。
动态存储变量在程序执行过程中使用它时才分配存储单元,使用完毕立即释放。
这样由于变量存储方式不同而产生的特性称为变量的生存期,生存期表示了变量存在的时间。
生存期和作用域是从时间上和空间这两个不同的角度来描述变量的特性。
变量的存储模型由作用域、键接点及存储期三个大属性来描述。
1.自动
变量的声明语法为
<存储类型> 数据类型 变是名
{int i,j,k;char c;//同下auto int i,j,k;auto char c;
}
自动变量具有以下特点
- 自动变量的作用域仅限于定义该变量的模块内。
- 自动变量属于动态存储方式,只有在定义该变量的函数被调用时才验它分配存储单元,开始它的生存期。
- 由于自动变量的作用域和生存期都局限定义它的模块内(函数或复合语句内),因此不同的模块中允许使用同名的变量而不会混淆。
2.寄存器
在一个代码块内(或一个函数头部作为参量)使用修饰符register声明的变量属于寄存器存储类。register修饰符暗示编译器程序相应的变量将频繁使用,如果可能的话,应将其保存在CPU的寄存器中,从而加快其存取速度。该类与自动存储类相似,具有自动存储期,代码块作用域和空链接。如查没有初始公,它的值也是不确定的。
使用register修饰有几点限制
- register变量必须是能被CPU寄存器所接受的类型,通常意味着register变量必须是一个单个的值,并且其长度应小于或等于整型的长度。
- 声明为register仅仅是一个请求,而非命令,因此变量仍然可能是普通的自动变量,没有放在寄存器中。
- 由于变量有可能存储在寄存器中,因此不能取地址运算符&获取register变量的地址。如果有这样的写法,编译器会报错。
- 只有局部变量和函数参数可以作为register变量,全局变量不行。
- 实际上有些系统并不把register变量存在寄存器中,而优化编译系统则可自动识别使用频繁的变量而把它们放在寄存器中。
3.静态、空链接
静态变量的类型说明符:static.在一个代码块内使用存储类修饰符static声明的局部变量属于静态空链接存储类。该类具有静态存储时期,代码块作用域和空链接。
静态变量的存储空间是在编译完成后就分配了,并且在程序运行的全部过程中都不会撤销。这里要区别的是,属于静态存储方式的变量不一定就是静态变量。
例:外部变量虽然属于静态存储方式,但不一定是静态变量,必须由static加以定义后才能称为静态外部变量,或称静态全局变量。
静态变量可分为静态局部变量和静态全局变量。
静态局部变量属于静态存储方式,它具有以下特点:
- 静态局部变量在函数内定义,它的生存期为整个程序执行期间,但是其作用域仍与自动变量相同,只能在定义变量的函数内使用该变量。 退出该函数后,尽管该变量还继续存在,但不能使用它。
- 可以对构造类静态局部量赋初值,例数组,若示赋初值,则由系统自动初始化为0。
- 基本数据类型的静态局部变量基在说明时未赋初值,则系统自动赋予0,。而对自动变量不赋初值,其值是不确定的。根据静态局部变量的特点,可以看出它是一种生存期为整个程序运行期的变量。虽然离开定义它的函数后不能使用,但如再次调用定义的函数进,它又可以继续使用,并保留上次初调用后的值。
因此,当多次调用一个函数且要求在调用定义它的函数时,可以考虑采静态局部变量。虽然用全局变量也可以达到上述目的,但全局变量有时会造成意外的副作用,因此仍以采用静态局部变量为宜。
4.静态、外部链接
未使用static修饰的全局变量属于静态、外部链接存储类。具有静态存储时期,文件作用域和外部链接。仅在编译时初始化一次。如未明确初始化,它的字节也被设定为0。使用外部变量的函数中使用extern关键字来再次声明。如果是在其文件中定义的,则必须使用extern.
5.静态、内部链接
全局变量在关键字之前再冠以static就构成了静态的全局变量,属于静态、内部链接存储类。与静态、外部链接存储类不同的是,具有内部链接,使得仅能被与它在同一个文件的函数使用。这样的变量也是仅在编译时初始化一次。如未明确初化,它的字节被设定为0。
这两者的区别在于非静态全局变量的作用域是整个程序,但当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的;而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一个源程序的其他源文件中不能使用。
由于静态全局变量的作用域局限于一个源文件内,只能被该源文件内的函数使用,因此可以避免在其他源文件中引起错误。
$$
非静态全局变量作用域
\begin{cases}
文件一 {静态全局变量作用域\
文件二 {静态全局变量作用域\
文件三 {静态全局变量作用域\
\end{cases}
$$
静态全局变量及非静态全局变量的区别。
分析:普通全局变量,是外部链接,可以被其他文件引用。有static关键字修饰的静太全局变量,是内部链接,限制链接,限制了变量只在当前文件使用。
2.4 预处理
所谓预处理是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所做的工作。预处理是C语言的一个重要功能,它由预处理负责完成。当编译一个程序时,系统将自动调用预处理程序对程序中的“#” 号开头的预处理部分时行处理。处理完毕之后可以进入源程序的编译阶段。
C语言提供了多种处理功能,如宏定义、文件包含、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
2.4.1预定义
在C语言源程式中允许用一个标识符来表示一串符号,称为宠,被定义为宏的标示识符称为宏名。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的符号串去替换,这称为宏替换或宏展开。
- 预定义符号
在C语言中, 有一些预处理定义的符号串,它们的值是字符串常量,或者是十进制数字常量,通常在调试程序时用于输出程序的各项信息。
符号 | 示例 | 含义 |
---|---|---|
__FILE__ | /home/david/hello.c | 正在预编译的源文件名 |
__LINE__ | 5 | 文件当前行的行号 |
__FUNCTION__ | main | 当前所在的函数名 |
__DATE__ | Mar 13 2009 | 预编译文件的日期 |
__TIME__ | 23:04:12 | 预编译文件的时间 |
__STDC__ | 1 | 如果编译器遵循ASIN C则值为1 |
#include<stdio.h>
int main()
{printf("The file is %s\n",__FILE__);printf("The line is %d\n",__LINE__);printf("The function is %s\n",__FUNCTION__);printf("The date is %s\n",__DATE__);printf("The time is %s\n",__TIME__);return 0}
- 宏定义
以上是C语言中自带的预定义符号,除此之外,用户自己也可以编写宏定义。宏定义是由源程序中的宏定义#define语句完成的,而宏替换是由预处理程序自动完成的。在C语言中,宏分为带参数和不带参数两种,
(1) 无参宏定义
#define 标识符 字符串
#表示这是一条预处理命令,凡是以#开头的均为预处理命令。
define 为宏定义命令
标识符为所定义的宏名
字符串可以是常数、表达式、格式串等
#define M (y+3)
这样就定义了M表达式为“(y+3)”,在此后编写程序时,所有的“(y+3)”都可由M代替,而对源程序编译时,将先由预处理程序进行宏替换,即用"(y+3)"表达式去替代所有的宏名M,然后再进行编译。
#include<stdio.h>
#include M (y + 3)
int main()
{int s,y;printf("input a number:")scnaf("%d",&y);s = 5 * M;printf("s = %d\n",s);return 0;
}
最后宏展开是s=5*(y+3)
对于宏定义还要说明以下几点:
- 宏定义用宏名来表示一串符号,在宏展开时又以该符号串取代宏名,这只是一种简单的替换,符号串中可以包含任何符,可以是常数,也可以是表达式,预处理程序对它不做任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。
- 宏定义不是声明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
- 宏定义的作用域包括从宏定义命名起到源程序结束,如要终止其作用域可以使用#undef命令来取消宏作用域。
#define PI 3.14159
func10
{//TODO
}
#undef PI
func20
{//TODO
}
- 宏定义在源程序中若用引号括起来,则预处理程序不对其进行宏替换。
#define OK 10
main()
{printf("OK");
}
- 宏定义允许嵌套,在宏定义的符号串中可以使用已经定义的宏名,在宏展开时由预处理程序层层替换。
- 习惯上宏名用大写字母表示,以便与变量区别,但也允许用小写字母表示。
- 对输出格式做宏定义,可以减少编写麻烦。
#incldue<stdio.h>
#define P printf
#define D "%d\n"
#define F "%f\n"
int main()
{int a = 5,c = 8,e = 11;float b = 3.8,d = 9.7,f = 21.08;P(D F,a,b);P(D F,c,d);P(D F,e,f);return 0;
}
(2)带参数宏定义
c语言允许宏带有参数,在宏定义中的参数称为形参数,在宏调用的参数称为实际参数。对参数的宏,在调用中不仅要宏展开,而且要用实参去替换形参数。
带参宠定义的一般形式。
#define 宏名(参数列) 字符串
在字符串中含有多个形参数。带参宏调用的一般形式。
宏名(实参表)
;
例如:
#define M(y) (y)+3 /*宏定义*/
若想调用宏,可以采用如下方法:
K=M(5);//宏调用
在宏调用时,用实参5代替宏定义中的形参数y,经预处理宏展开后的语句为
K=5+3
以下是这段程序就是常见的比较两个数大小的宏表示,
#include<stdion.h>
#define MAX(a,b) (a>b)?a:b//宏定义
int main()
{int x = 10,y = 20,max;max = MAX(x,y);printf("max=%d",max);
}
由于宏定义非常容易出错,因此,对于带参的宏定义有以下问题需要特别说明。
- 带参数宏定义,宏名和形参表之间不能有空格出现。
- 在带参数宏定义,形式参数不分配内存单元,因此不必做类型定义。这与函数中的情竞是不同的。
速带数宏定义,只是符号替换,不存在值传递的问题。 - 在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。
#define SQ(y) (y)*(y)
sq=SQ(a+1)
- 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
#define SQ(y) y*y
sq = SQ(a+1);//a+1*a+1;//计算顺序错误
#define SQ(y) (y)*(y)
sq = 160/SQ(a+1);//160/(a+1)*(a+1)计算顺序错误
#define SQ(y) ((y)*(y))
sq = 160/SQ(a+1);//160/((a+1)*(a+1))
函数与宏调用区别
#include<stdio.h>
/*程序1,函数调用*/
int SQ(int y)
{return(y*y);
}int main()
{int i = 1;while(i<=5){printf("%d",SQ(i++));/*函数调用*/}return 0;
}
//~ 1 4 9 16 25
#include <stdio.h>
#define SQ(y) ((y)*(y))
int main()
{int i = 1;while(i<=5){printf("%d",SQ(i++));//((i++)*(i++))加加了两次}return 0;
}
//~ 1 9 25
属性 | 宏 | 函数 |
---|---|---|
处理阶段 | 预处理阶段,只是符号串的简单的替换 | 编译阶段 |
代码长度 | 每次使用宏时,宏代码都被插入到程序中。因此,除了非常小的宏之外程序长度也会大幅度增长 | (除了inline)函数代码只出现在一个地方,每次使用这个函数,都只调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数调用/返回的额外开销(inline函数除外) |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境中,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果 | 函数参数只在函数调用时求值一次,它的结果值传递给函数,因此,表达式的求值结果更容易预测 |
参数求值 | 参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果 | 参数在函数被调用前只求值一次,在函数中多次使用参数并不会导致多种求值问题,参数的副作用不会造成任何特殊的问题 |
参数类型 | 宏与类型无关,只要对参数的操作是合法的,它可以使用任何参数类型。 | 函数的参数与类型有关,如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的。 |
2.4.2 文件包含
文件包含是C语言预处理的另一个重要功能,文件包含命令行的一般形式为
#include "文件名"
例
#include<stdio.h>
#include<math.h>
文侦探包含语句的功能是把指定的文件插入该语句位置,从而把指定的文件和当前的源程序文件连成一个源文件。在程序设计中,文件包含是很有用的。一个大的程序可以分为多个模块,由多个程序员分别编写。有些公用的符号常量、宏、结构、函数等的声明或定义可单独组成一个文件,在其他文侦探的开头用包含命令包含该文件即可使用。这样,可避免在每个文件开头都去写那些公用量,从而节省时间并减少错识。
对文件包含作以下说明
- 包含命令中的文件名可以用双引号括起来,也可以用尖括号括起来,例如以下写法是允许的
#include "stdio.h"
#include <math.h>
但是这两种形式是有区别的:使用尖括号表示在系统头文件目录中去查找(头文件目录可以由用户来指定);
但使用双引号则表示首先在当前源文件目录中查找,若未找到才到系统头文件目录中去查找。用户编程时可以根据自己文件所在的位置来选择其一种形式。
2. 一个include命令只能指定一个被包含文件,若有多个文件要包含,则需要多个include命令。
3. 文件包含允许嵌套,即在一个被包含的文件中又可以包含别的文件。
2.4.3 条件编译
预处理程序提供了条件编译的功能,可以按不同的条件去编译不同的程序代码,从而产生不同的目标代码文件,这对于程序的移值和调试很有用的。
条件编译的三种形式
- 第一种形式
#ifdef //标识符
//TODO 程序段1
#else
//TODO 程序段2
#endif
它的功能是如果标识符已被#define语句定义过,则编译程序段1,否则编译程序段2.如果程序段2为空,本格式中的#else可以没有。
#include <stdio.h>
#define NUM OK /*宏定义*/
int main()
{struct stu{int num;char *name;float score;}*ps;ps = (struct stu*)malloc(sizeof(struct stu));ps->num = 102;ps->name = "David";ps->score = 92.5;
#ifdef NUM /*条件编译,若定义了NUM,则打印以下内容*/printf("Number = %d\nScore=%f\n",gs->num,ps->score);
#else /*右没有定义NUM,则打印以下内容*/printf("Name = %s\n",ps->name);
#endiffree(ps);return 0;
}
/**
Number = 102
Score = 95.5000000
**/
在程序中根据NUM是否被定义来决定编译哪一个printf语句。因为在程序的第二行中定义了宏NUM,因此应编译第一个printf语句,运行结果则为输出学号和成绩。
在此程序中,宏NUM是符号串 “OK”的别名,其实也可以为任何符号串,甚至不给出任何符号串,
#define NUM
在调试时,可以将要打印的信息用#ifdef __DEBUG__
语句包含起来,在调试完成之后,可以直接去掉宏定义#define __DEBUG__
,这样就可以做臧产品的发布版本了。条件编译语句和宏定义语句一样,在#ifdef语句后不能加分号(;)
2. 第二种形式
#ifndef 标识符
程序段1
#else
程序段2
#endif
与第一种形式的区别是将ifdef改成ifndef。它的功能是,如果标识符未被#define语句定义过,则编译程序段1,否则编译程序段2,定与第一种形式的功能正好相反。
- 第三种形式
#if 常量表达式
//TODO程序段1
#else
//程序式2
//endif
它的功能是,如果常量表达式的值为直为(非0),则编译程序段1,否则编译程序段2。因此可以使程序在不同的条件下,完成不同的功能。
#include <stdio.h>
#define IS_CURCLE 1
int main()
{float c = 2,r,s;
#if IS_CURCLEr = 3.14159*c*c;printf("area of round is :%f\n",r);
#else s = c*c;printf("area of square is:%f\n",s);
#endifreturn 0;
}
本例中采用了第三种形式的条件编译。在程序宏定义,将IS_CURCLE定义为1,因化在条件编译时,只编译计算和输出圆的面积代码部分。
上面价绍的程序的功能可以使用条件语句来实现,但是使用条件语句将会对整个源程序进行编译,生成的目标代程程序很长,也比较麻烦,采用条件编译,则可以根据编译条件选择性地进行编译,生成白目标程序较短,尤其在调试和发布不同版本时非常有用。
2.5 需要注意的问题
嵌入式开发很重要的一个问题就是可移植性问题。Linux是一个可移植性非常好的系统,这也是嵌入式linux能够迅速发展起来的一个主要原因。所以,嵌入式Linux可以移植性方面所做的工作是非常值得学习的。本节结合嵌入式Linux能够迅速发展起来的一个主要原因。所以嵌入式Linux在可移植性方面所做的工作是非常值得学习的。
2.5.1字长和数据类型
能够由计算机一次完居处理的数据称为字,不同体系结构的字长通常会有所区别,例如,现在通用的处理器字长为32位。
为了解愉不同的体系结构有不同字长的问题,嵌入式Linux中给出两种数据类型。其中一是不透明的数据类型,其二是长度明确的数据类型。
不透明数据类型隐藏了它们的内部格式或结构。在C语言中,它门就像黑盒一样,开发者们利用typedef声明一个类型,把它们叫作不透明数据类型,并希望其他开发者不要重新将其转化为对应的那个标准C语言类型。
例如用来保存进程标识符的pid_t类型的实际长度就被隐藏起来了,尽管任何人都可以揭开它的面纱,因为其实它就是一个int类型。
长度明确的数据类型也很常见。作为一个程序员,通常在程序中需要操作硬件设备,这时就必须明确知道数据的长度。
嵌入式Linux内核在“asm/types.sh”中定义了这些长度明确的类型
类型 | 描述 | 类型 | 描述 |
---|---|---|---|
s8 | 带符号字节 | s32 | 带符号32位整数 |
u8 | 不带符号字节 | u32 | 不带符号32位整数 |
s16 | 带符号16位整数 | s64 | 带符号64位整数 |
u16 | 不带符号16位整数 | u64 | 不带符号64位整数 |
2.5.2数据对齐
对齐是内存数据与内存中的相对位置相关的问题,如查一个变量的内存地址正好是它长度的整数倍,它就被称作是自对齐。例如,对于一个32位(4个字节)类型的数据,如果它在内存中的地址刚好可以被4整除(最低两位是0)那它是自然对齐。
一些体系统构对对齐的要求非常严格。通常基于RISC的体系统载入未对齐的数据会导致处理器陷入一种可处理的错误,还有一些系统可以访问没有对齐的数据,但性能会下降。编写可移植性高的代码要避免对齐问题,保证所有的类型都能够自然对齐。
2.5.3 字节序
字节顺序是指一个字中各个字节的顺序,有大端模式(big-endian)和小端模式(little-endian)大端模式是指在这种格式中,字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。小端模式与大端存储格式相反,在小端存储格式中,低地址中存放的是字数据的低字节,高地址存放的是字数据的高字节。
ARM体系结构支持大端模式和小端模式两内存模式。
请看下面的一段代码,通过这段代码我们可以查看一个字(通常为4个字节)数据的每个字节在内存中的分布情况,即可以分辨出当前系统采用哪种字节顺序模式
typedef usigned char byte;
typedef usigned int word;
word val32 = 0x87654321;
byte val8 = *((byte*)&val32)
这段代码在小端模式和大端模式下其运行结果分别为val8=0x21和val8=0x87。其实,变量val8所在的地方是val32的低地址。因此val8值为val32的低字节(0x21),则本系统是小端模式;若val8值为val32的高字节(0x87),则本系统是大端模式。
地址示例 | |||
---|---|---|---|
高地址 | 0x87 | 0xbfc3809b | 0x21 |
0x65 | 0xbfc3809a | 0x43 | |
0x43 | 0xbfc38099 | 0x65 | |
低地址 | 0x21 | 0xbfc38098 | 0x87 |
小端 | 大端 |