你要学习的,不仅仅是C语言……
对于上面的几个C语言测试,如果你已经知道了答案,并且知道其要考查的是什么知识点,恭喜你,你对C语言及计算机体系结构的知识已经很熟悉了。如果回答得不是很好,偷偷用百度也没有搜到理想的答案,也不用气馁,因为这次测试要考查的内容其实已经不仅仅是C语言的知识了,而是和嵌入式C语言开发相关的一些理论知识,如处理器架构、操作系统、编译原理、编译器特性、内存堆栈管理、Linux内核中的GNU C扩展语法等。
当然,上面的测试也不是为了故意扎你心或者卖关子,让你赶紧掏腰包买下这本书,而是想要传递一个信息:要想从事嵌入式开发工作,尤其是嵌入式Linux内核驱动开发工作,你要精通的不仅仅是C语言,好还要掌握和C语言相关的一系列基础理论和调试技能。笔者也是过来人,从初学习嵌入式到从事嵌入式开发工作,这一路走来坎坷崎岖,什么都不说了,说多了都是泪。从一开始连指针都不会用、不敢用,看内核驱动代码一头雾水,越看越没底、越看越没自信,到现在不再犯怵,有自信和能力看懂内核中的代码细节和系统框架,这种进步不是天上掉下来的,也不是一不小心跌入山洞,捡到武功秘籍练出来的,而是不断地学习和实践、反复迭代、不断完善自己的知识体系和技能树,才慢慢达到的。学习没有捷径可走,要想真正学好嵌入式、精通嵌入式,个人觉得除了精通C语言,好还要具备以下完整的知识体系和编程技能。
l 半导体基础、CPU工作原理、硬件电路、计算机系统结构。
l ARM体系结构与汇编指令、汇编程序设计、ARM反汇编分析。
l 程序的编译、链接、安装、运行和重定位分析。
l 熟悉C语言标准、ARM、GNU编译器的特性和扩展语法。
l C语言的模块化编程思想,学会使用模块化思想去分析复杂的系统。
l C语言的面向对象编程(简称OOP)思想,学会使用OOP思想去分析Linux内核驱动。
l 对指针的深刻理解,对复杂指针的声明和灵活应用。
l 对内存堆栈管理、内存泄漏、栈溢出、段错误的深刻理解。
l 多任务并发编程思想,CPU和操作系统基础理论。
本书内容及写作初衷
本书从C语言的角度出发,分10章,在默认读者已经掌握C语言基本语法的基础上,和大家一起探讨、学习C语言背后的CPU工作原理、计算机体系结构、ARM平台下程序的编译/链接、程序运行时的内存堆栈管理等底层知识。同时,针对嵌入式开发领域,用3章分别探讨了C语言的面向对象编程思想、模块化编程思想和多任务编程思想,这些底层知识和编程思想构成了嵌入式开发所需要的通用理论基础和核心技能。尤其是对于很多从不同专业转行到嵌入式开发的朋友,由于专业背景的差异,导致每个人的知识储备和编程技能树参差不齐,在学习嵌入式开发的过程中会经常遇到各种各样的问题,陷入学习的困境。
本书的写作初衷就是为不同专业背景的读者搭建嵌入式开发所需要的完整知识体系和认知框架。掌握了这些基础理论和编程技能,也就补齐了短板,可为后续的嵌入式开发进阶学习打下坚实的基础。
本书特色
l 大白话写作风格,通俗易懂,不怕学不会,就怕你不学。
l 大量的配图、原理图,图文并茂,更加有利于学习和理解。
l 在ARM平台下讲解程序的编译、链接和运行原理(独创)。
l 现场“手撕”ARM汇编代码,从反汇编角度剖析C函数调用、传参过程。
l 多角度剖析C语言:CPU、计算机体系结构、编译器、操作系统、软件工程。
l GNU C编译器扩展语法精讲(在GNU开源软件、Linux内核中大量使用)。
l 内存堆栈管理机制的底层剖析,从根源上理解内存错误。
l 从零开始一步一步搭建和迭代嵌入式软件框架。
l 教你用OOP思想分析Linux内核中复杂的驱动和子系统。
l C语言的多任务并发编程思想,CPU和操作系统零基础入门。
读者定位
本书针对的是嵌入式开发,尤其是嵌入式Linux开发背景下的C语言进阶学习,比较适合在校学生、嵌入式学员、工作1~3年的职场新兵阅读和学习。为了达到更好的学习效果,在阅读本书之前,首先要确保你已经掌握了C语言的基本语法,并且至少使用过一款C语言集成开发环境(VC 6.0、Visual Studio、C-Free、GCC都可以),开发过一个完整的C语言项目(课程设计也算)。有了这些基础和编程经验之后,学习效果会更好。
5.7.1 总有一个Bug,让你泪流满面
总有一种兴奋让你不能自抑,花枝乱颤;总有一个Bug让你夙夜难眠,泪流满面。当一个Bug让你毫无头绪,让你调到天昏地暗,到后几乎要放弃,开始漫无目的地乱改代码,祈求奇迹出现时,说明你需要休息一下了:出去转一转,吹个风,说不定灵感乍现,一下子又有了思路……
发生段错误的根本原因在于非法访问内存,即访问了权限未许可的内存空间。在日常编程中,有哪些行为会引发段错误呢?
常见的错误行为是访问内存禁区。如前面的图5-47所示的内核空间、零地址、堆和mmap区域之间的内存空间,这部分地址空间要么被内核占用,要么还处于“未开发”状态,需要申请才能使用。这就和城郊的荒地一样,你不能一看空着就跑过来盖房子,你需要先获得土地的使用权。
int main (void)
{
int i;
i = *(int *)0x8048000; //代码段只能读,不能写
*(int *)0x8048000 = 100; //段错误
i = *(int *)0x0; //段错误
return 0;
}
当我们往一个只读区域的地址空间执行写操作时,或者访问一个禁止访问的地址(如零地址)时,都会发生段错误。在实际编程中,总会因为各种各样的疏忽不小心触碰到这些“红线”,导致段错误。
#include
int main (void)
{
char *p;
*p = 1;
return 0;
}
编译运行上面的程序,可能正常运行,也可能会发生段错误。在函数内定义的局部变量如果未初始化,它的值是随机的,如果你人品大爆发,这个地址处于安全访问区,则向这个地址写数据是没有大问题的,至少不会报段错误。如果你运气不好,这个随机值正好处在内核空间,你再向这个地址写数据,则程序会立刻发生段错误并终止运行。
在我们调试链表时,通常通过指针来操作每一个节点。如果指针在遍历链表时已经指向链表的末尾或头部,指针已经指向NULL了,此时再通过该指针去访问节点的成员,就相当于访问零地址了,也会发生一个段错误,这个指针也就变成了非法指针。
在Linux环境下,每一个用户进程默认有8MB大小的栈空间,如果你在函数内定义大容量的数组或局部变量,就可能造成栈溢出,也会引发一个段错误。内核中的线程也是如此,每一个内核线程只有8KB的内核栈,在实际使用中也要非常小心,防止堆栈溢出。
在访问数组时,如果超越数组的边界继续访问,也会发生一个段错误。我们使用malloc()申请的堆内存,如果不小心多次使用free()进行释放,通常也会触发一个段错误。
//double_free.c
#include
int main (void)
{
char *p;
p = (char *) malloc (64);
free(p);
free(p); //引发段错误
return 0;
}