《2014大疆嵌入式笔试题》,这一份应该是全网搜得到的关于大疆嵌入式最完整的一份试题了。只可惜,这一份试题,网上也只是有题目,却一直没有发现完整的答案什么的。所以下面的解答主要都是博主结合网上的一些解答,总结出的见解和解答,如果有什么错误,还请指出,谢谢!
2014大疆嵌入式笔试题试题
编程基础
1、有如下CAT_s结构体定义,回答:
1)在一台64位的机器上,使用32位编译,Garfield变量占用多少内存空间?64位编译又是如何?(总分5分)
2)使用32位编译情况下,给出一种判断所使用机器大小端的方法。(总分5分)
struct CAT_s
{
int ld;
char Color;
unsigned short Age;
char *Name;
void(*Jump)(void);
}Garfield;
2、描述下面XXX这个宏的作用。(总分10分)
#define offsetof(TYPE,MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#define XXX(ptr,type,member)({\
const typeof(((type*)0)->member) *__mptr=(ptr);\
(type*)((char*)__mptr – offsetof(type,member));})
3、简述C函数:
1)参数如何传递(__cdecl调用方式);
2)返回值如何传递;
3)调用后如何返回到调用前的下一条指令执行。(总分10分)
4、在一个多任务嵌入式系统中,有一个CPU可直接寻址的32位寄存器REGn,地址为0x1F000010,编写一个安全的函数,将寄存器REGn的指定位反转(要求保持其他bit的值不变)。(总分10分)
5、有10000个正整数,每个数的取值范围均在1到1000之间,编程找出从小到大排在第3400(从0开始算起)的那个数,将此数的值返回,要求不使用排序实现。(总分10分)
嵌入式基本知识
1、简述处理器中断处理的过程(中断向量、中断保护现场、中断嵌套、中断返回等)。(总分10分)
2、简述处理器在读内存的过程中,CPU核、cache、MMU如何协同工作?画出CPU核、cache、MMU、内存之间的关系示意图加以说明(可以以你熟悉的处理器为例)。(总分10分)
基本通信知识
1、请说明总线接口USRT、I2C、USB的异同点(串/并、速度、全/半双工、总线拓扑等)。(总分5分)
2、列举你所知道的linux内核态和用户态之间的通信方式并给出你认为效率最高的方式,说明理由。(总分5分)
系统设计
有一个使用UART进行通信的子系统X,其中UART0进行数据包接收和回复,UART1进行数据包转发。子系统X的通信模块职责是从UART0接收数据包,如果为本地数据包(receiver 为子系统X),则解析数据包中的命令码(2字节)和数据域(0~128字节),根据命令码调用内部的处理程序,并将处理结果通过UART0回复给发送端,如果非本地数据包,则通过UART1转发。如果由你来设计子系统X的通信模块:
1)请设计通信数据包格式,并说明各字段的定义;(总分5分)
2)在一个实时操作系统中,你会如何部署模块中的任务和缓存数据,画出任务间的数据流视图加以说明;(总分5分)
3)你会如何设置任务的优先级,说说优缺点;(总分5分)
4)如果将命令码对应的处理优先级分为高、低两个等级,你又会如何设计;(总分5分)
试题解答
编程基础
1、有如下CAT_s结构体定义,回答:
1)在一台64位的机器上,使用32位编译,Garfield变量占用多少内存空间?64位编译又是如何?(总分5分)
2)使用32位编译情况下,给出一种判断所使用机器大小端的方法。(总分5分)
struct CAT_s
{
int ld;
char Color;
unsigned short Age;
char *Name;
void(*Jump)(void);
}Garfield;
解答:1)这是一条很经典的判断结构体大小的题目,经典到只要面试C语言或者C++的工作,无论是嵌入式还是互联网公司,绝大多数情况下都会问到的一种题型。
这题主要考的就是:内存对齐。
对于大多数的程序员来说,内存对齐基本上是透明的,这是编译器该干的活,编译器为程序中的每个数据单元安排在合适的位置上,从而导致了相同的变量,不同声明顺序的结构体大小的不同。
那么编译器为什么要进行内存对齐呢?主要是为了性能和平台移植等因素,编译器对数据结构进行了内存对齐。
为了分析造成这种现象的原因,我们不得不提及内存对齐的3大规则:
- 对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍;
- 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍;
- 如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型。
按照上面的3大规则直接来进行分析:
- 使用32位编译,int占4, char 占1, unsigned short int占2,char*占4,函数指针占4个,由于是32位编译是4字节对齐,所以该结构体占16个字节。(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是int是最长的字节,所以按4字节对齐);
- 使用64位编译 ,int占4, char 占1, unsigned short int占2,char*占8,函数指针占8个,由于是64位编译是8字节对齐,(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是函数指针是最长的字节,所以按8字节对齐)所以该结构体占24个字节。
下面说明一下不同平台数据类型与所占字节表:
数据类型 | 32位 | 64位 | 备注 |
char | 1 | 1 | |
short | 2 | 2 | |
int | 4 | 4 | |
long | 4 | 8 | 32位与64位不同 |
float | 4 | 4 | |
char* | 4 | 8 | 其他指针类型如long *, int * 也是如此 |
long long | 8 | 8 | |
double | 8 | 8 | |
long double | 10/12 | 10/16 | 有效位10字节。32位为了对齐实际分配12字节;64位分配16字节 |
参考文章:内存对齐规则之我见。
2)大端小端问题:
- 所谓的大端模式(BE big-endian),是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中(低对高,高对高);
- 所谓的小端模式(LE little-endian),是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中(低对低,高对高)。
为什么要有大小端区分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
首先给出一个方法(Linux操作系统的源码中的判断):
static union {
char c[4];
unsigned long mylong;
} endian_test = {{ 'l', '?', '?', 'b' } };
#define ENDIANNESS ((char)endian_test.mylong)
这是利用:联合体union的存放顺序是所有成员都从低地址开始存放的特性。由于联合体的各个成员共用内存,并应该同时只能有一个成员得到这块内存的使用权(即对内存的读写)。如果是“l”(小端)、“b”(大端)。
除了这种方法之外,也可以利用数据类型转换的截断特性:
void Judge_duan()
{
int a = 1; //定义为1是为了方便 如果你喜欢你可以随意,
//只要你能搞清楚 例如:0x11223344;
char *p = (char *)&a;//在这里将整形a的地址转化为char*;
//方便后面取一个字节内容
if(*p == 1)//在这里解引用了p的一个字节的内容与1进行比较;
printf("小端\n");
else
printf("大端\n");
}
参考文章: 关于机器大小端的判定。
顺便提一下,大小端之间的转化,以小端转化为大端为例:
int endian_convert(int t)
{
int result=0;
int i;
for (i = 0; i < sizeof(t); i++) {
result <<= 8;
result |= (t & 0xFF);
t >>= 8;
}
return result;
}
2、描述下面XXX这个宏的作用。(总分10分)
#define offsetof(TYPE,MEMBER)((size_t)&((TYPE*)0)->MEMBER)
#define XXX(ptr,type,member)({\
const typeof(((type*)0)->member)*__mptr=(ptr);\
(type*)((char*)__mptr – offsetof(type,member));})
解答:其实这个宏定义是Linux内核结构体container_of的宏定义,几乎没修改过(该宏定义在kernel.h中)。当然,我们先认为这是一个陌生的宏定义来进行分析它的作用。
首先你得能看出来这是两个宏定义……,第二个宏定义是换行的宏定义方式,以“\”结尾。
先来看第一个宏定义:
#define offsetof(TYPE,MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
- (TYPE*)0:将0强转为TYPE类型的指针,且指向了0地址空间;
- (TYPE*)0->MEMEBER:指向结构体中的成员;
- &((TYPE*)0->MEMBER):获取成员在结构体的位置,因为起始为0,所以获取的地址即为实际的偏移地址。
分析:(TYPE *)0,将 0 强制转换为 TYPE 型指针,记 p = (TYPE *)0,p是指向TYPE的指针,它的值是0。那么 p->MEMBER 就是 MEMBER 这个元素了,而&(p->MEMBER)就是MENBER的地址,而基地址为0,这样就巧妙的转化为了TYPE中的偏移量。再把结果强制转换为size_t型的就OK了,size_t其实也就是unsigned int。
再来看需要我们描述功能的宏定义XXX:
#define XXX(ptr,type,member)({\
const typeof(((type*)0)->member) *__mptr=(ptr);\
(type*)((char*)__mptr – offsetof(type,member));})
- typeof构造的主要应用是用在宏定义中。可以使用typeof关键字来引用宏参数的类型。也就是说,typeof(((type*)0)->member)是引用与type结构体的member成员的数据类型;
- 获得了数据类型之后,定义一个与type结构体的member成员相同的类型的指针变量__mptr,且将ptr值赋给它;
- 用宏offsetof(type,member),获取member成员在type结构中的偏移量;
- 最后将__mptr值减去这个偏移量,就得到这个结构变量的地址了(亦指针)。
XXX宏的实现思路:计算type结构体成员member在结构体中的偏移量,然后ptr的地址减去这个偏移量,就得出type结构变量的首地址。
具体的功能就是:ptr是指向正被使用的某类型变量指针;type是包含ptr指向的变量类型的结构类型;member是type结构体中的成员,类型与ptr指向的变量类型一样。功能是计算返回包含ptr指向的变量所在的type类型结构变量的指针。
参考文章:老生常谈的Linux内核中常用的两个宏定义。
3、简述C函数:
1)参数如何传递(__cdecl调用方式);
2)返回值如何传递;
3)调用后如何返回到调用前的下一条指令执行。(总分10分)
解答:1)参数如何传递 (__cdecl调用方式) :
__cdecl是C Declaration的缩写(declaration,声明),是C语言中函数调用约定中的一种(默认)。
什么是函数调用约定?
当一个函数被调用时,函数的参数会被传递给被调用的函数,同时函数的返回值会被返回给调用函数。函数的调用约定就是用来描述参数(返回值)是怎么传递并且由谁来平衡堆栈的。也就是说:函数调用约定不仅决定了发生函数调用时函数参数的入栈顺序,还决定了是由调用者函数还是被调用函数负责清除栈中的参数,还原堆栈。
常见的函数调用约定有:__stdcall,__cdecl(默认),__fastcall,__thiscall,__pascal等等。
它们按参数的传递顺序对这些约定可划分为:
- 从右到左依次入栈:__stdcall,__cdecl,__thiscall;
- 从左到右依次入栈:__pascal,__fastcall。
下面比较一下最为常见的两种:
- __cdecl:是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,由调用者负责把参数压入栈,最后也是由调用者负责清除栈的内容 ;
- __stdcall:是StandardCall的缩写,是C++的标准调用方式:所有参数从右到左依次入栈,由调用者负责把参数压入栈,最后由被调用者负责清除栈的内容。
另外,还要注意的是,如printf此类支持可变参数的函数,由于不知道调用者会传递多少个参数,也不知道会压多少个参数入栈,因此函数本身内部不可能清理堆栈,只能由调用者清理了。
也就是说:支持可变参数的函数调用约定:__cdecl,带有可变参数的函数必须是cdecl调用约定,由函数的调用者来清除栈,参数入栈的顺序是从右到左。由于每次函数调用都要由编译器产生清除(还原)堆栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多。
2)C语言返回值如何传递:
一般情况下,函数返回值是通过eax进行传递的,但是eax只能存储4个字节的信息,对于那些返回值大于4个字节的函数,返回值是如何传递的呢?
假设返回值大小为M字节:
- M <= 4字节,将返回值存储在eax返回;
- 4 < M <=8,把eax,edx联合起来。其中,edx存储高位,eax存储低位;
- M > 8,如何传递呢?用一下代码测试:
typedef struct big_thing
{
char buf[128];
}big_thing;
big_thing return_test()
{
big_thing b;
b.buf[] = 0;
return b;
}
int main()
{
big_thing n = return_test();
}
- 首先main函数在栈额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,这里称为temp;
- 将temp对象的地址作为隐藏参数传递个return_test函数;
- return_test 函数将数据拷贝给temp对象,并将temp对象的地址用eax传出;
- return_test返回以后,mian函数将eax指向的temp对象的内容拷贝给n。
也就是说,如果返回值的类型的尺寸太大,c语言在函数的返回时会使用一个临时的栈上内存作为中转,结果返回值对象会被拷贝两次。整个过程使用的是指向返回值的指针来进行拷贝的,而指针本身是通过eax返回的。因而不到万不得已,不要轻易返回大尺寸对象。
那么上面的eax、edx是什么呢?
- eax是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器;
- ebx 是"基地址"(base)寄存器, 在内存寻址时存放基地址;
- ecx是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器;
- edx则总是被用来放整数除法产生的余数。
函数返回值为什么一般放在寄存器中?
这主要是为了支持中断;如果放在堆栈中有可能因为中断而被覆盖。
参考文章: C函数参数传递与返回值传递。
3)C语言调用后如何返回到调用前的下一条指令执行:
这就涉及到函数的堆栈帧这个概念。
栈在程序运行中具有举足轻重的地位。最重要的,栈保存了一个函数调用所需要的维护信息,被称为堆栈帧(Stack Frame),一个函数(被调函数)的堆栈帧一般包括下面几个方面的内容:
函数的参数;函数的局部变量;寄存器的值(用以恢复寄存器);函数的返回地址以及用于结构化异常处理的数据(当函数中有try…catch语句时才有)等等。
由于在函数的堆栈帧中存放了函数的返回地址,即调用方调用此函数的下一条指令的地址。故而,在函数调用后,由函数调用方执行,直接返回调用前的下一条指令。
参考文章:浅谈C/C++堆栈指引——C/C++堆栈很强大(绝美)。
4、在一个多任务嵌入式系统中,有一个CPU可直接寻址的32位寄存器REGn,地址为0x1F000010,编写一个安全的函数,将寄存器REGn的指定位反转(要求保持其他bit的值不变)。(总分10分)
解答:指定为反转用异或,多任务嵌入式系统保证安全性!
void bit_reverse(uint32_t nbit)
{
*((volatile unsigned int *)0x1F000010) ^= (0x01 << nbit);
}
对于位运算的一些小总结:特定位清零用&;特定位置1用|;所有位取反用~;特定位取反用^。
参考文章:2.2.位与位或位异或在操作寄存器时的特殊作用。
5、有10000个正整数,每个数的取值范围均在1到1000之间,编程找出从小到大排在第3400(从0开始算起)的那个数,将此数的值返回,要求不使用排序实现。(总分10分)
解答:10000个正整数找出从小到大的第3400个(从0开始算起),第一个想到的就是排序(冒泡排序、插入排序、选择排序……),或者使用桶排序。但这些显然都不满足这个题目的要求。
关键的点是:正整数,每个数的取值均在1-1000之间,这是本题一个特殊性。
本题思路:维护一个数组count[1000],分别存储1-1000每个数字的出现次数。
#include <iostream>
using namespace std;
#define TOTAL 10000
#define RANGE 1000
#define REQUIRED 3400
int main()
{
int number[TOTAL] = { 0 };
int count[RANGE] = { 0 };
int i, sum = 0;
for (i = 0; i < 10000; i++) {
number[i] = (rand() % 1000) + 1; /*产生10000个1-1000之间的随机数*/
}
for (i = 0; i < 10000; i++) {
count[number[i] - 1]++; /*计算10000个整数出现次数*/
}
for (i = 0; i < 1000; i++) {
sum += count[i];
if (sum >= REQUIRED + 1) {
cout << i + 1 << endl;
break;
}
}
return 0;
}
下篇的笔试题解析链接:【机试题】2014大疆嵌入式笔试题(附超详细解答,下篇)。