描述
开 本: 128开纸 张: 胶版纸包 装: 平装-胶订是否套装: 否国际标准书号ISBN: 9787302460787丛书名: 面向“工程教育认证”计算机系列课程规划教材
本书兼顾教学、科研和工程开发的需要,既可以作为各类院校嵌入式方向的本科生和研究生的嵌入式系统教材,也可以作为嵌入式系统开发工程师的参考书。
目录
上篇原
理 部 分
第1章嵌入式系统概述
1.1嵌入式系统简介
1.1.1嵌入式系统历史与现状
1.1.2嵌入式系统体系结构
1.1.3应用领域和发展方向
1.2嵌入式处理器
1.2.1嵌入式处理器简介
1.2.2ARM处理器的应用领域及一般特点
1.2.3ARM处理器系列
1.3嵌入式操作系统
1.3.1嵌入式操作系统简介
1.3.2嵌入式Linux
1.3.3VxWorks
1.3.4μC/OSⅡ
1.3.5Windows CE
1.3.6Symbian
1.3.7Android
1.3.8iOS
1.3.9其他嵌入式操作系统
1.4嵌入式系统设计
1.4.1嵌入式系统设计过程
1.4.2硬件设计平台的选择
1.4.3软件设计平台的选择
1.4.3嵌入式应用软件开发
1.4.5测试和优化
小结
进一步探索
第2章ARM处理器和指令集
2.1ARM处理器简介
2.1.1ARM公司和ARM产品简介
2.1.2ARM指令集体系结构版本
2.1.3ARM处理器系列
2.2ARM指令集简介
2.2.1RISC简介
2.2.2ARM状态和Thumb状态
2.2.3ARM指令类型和指令的条件域
2.3ARM指令的寻址方式
2.3.1立即寻址
2.3.2寄存器寻址
2.3.3寄存器偏移寻址
2.3.4寄存器间接寻址
2.3.5基址变址寻址
2.3.6多寄存器寻址
2.3.7堆栈寻址
2.3.8相对寻址
2.4ARM指令简介
2.4.1跳转指令
2.4.2通用数据处理指令
2.4.3乘法指令
2.4.4Load/Store内存访问指令
2.4.5ARM协处理器指令
2.4.6杂项指令
2.4.7饱和算术指令
2.4.8ARM伪指令
2.5Thumb指令简介
2.5.1Thumb跳转指令
2.5.2Thumb通用数据处理指令
2.5.3Thumb算术指令
2.5.4Thumb内存访问指令
2.5.5Thumb软中断和断电指令
2.5.6Thumb伪指令
小结
进一步探索
第3章嵌入式Linux操作系统
3.1嵌入式Linux简介
3.1.1μCLinux
3.1.2RTLinux
3.1.3红旗嵌入式Linux
3.2内存管理
3.2.1内存管理和MMU
3.2.2标准Linux的内存管理
3.2.3μCLinux的内存管理
3.3进程管理
3.3.1进程和进程管理
3.3.2RTLinux的进程管理
3.3.3标准Linux的进程管理
3.3.4μCLinux的进程管理
3.4文件系统
3.4.1文件系统定义
3.4.2Linux文件系统
3.4.3嵌入式Linux文件系统
小结
进一步探索
第4章嵌入式软件编程技术
4.1嵌入式编程基础
4.1.1嵌入式汇编语言基础
4.1.2嵌入式高级编程知识
4.1.3嵌入式开发工程
4.2嵌入式汇编编程技术
4.2.1基本语法
4.2.2汇编语言程序设计案例
4.3嵌入式高级编程技术
4.3.1函数可重入
4.3.2中断处理过程
4.4高级语言与汇编语言混合编程
4.4.1高级语言与汇编语言混合编程概述
4.4.2汇编程序调用C程序
4.4.3C程序调用汇编程序
小结
进一步探索
第5章开发环境和调试技术
5.1交叉开发模式概述
5.2宿主机环境
5.2.1串口终端
5.2.2BOOTP
5.2.3TFTP
5.2.4交叉编译
5.3目标板环境
5.3.1JTAG接口简介
5.3.2Boot Loader简介
5.4交叉编译工具链
5.4.1交叉编译的构建
5.4.2相关工具
5.5gdb调试器
5.6远程调试
5.6.1远程调试原理
5.6.2gdb远程调试功能
5.6.3使用gdbserver
5.7内核调试
5.7.1内核调试技术
5.7.2kgdb内核调试
5.8网络调试
小结
进一步探索
第6章Boot Loader技术
6.1Boot Loader基本概念
6.1.1Boot Loader所支持的硬件环境
6.1.2Boot Loader的安装地址
6.1.3Boot Loader相关的设备和基址
6.1.4Boot Loader的启动过程
6.1.5Boot Loader的操作模式
6.1.6Boot Loader与主机之间的通信设备及协议
6.2Boot Loader典型结构
6.2.1Boot Loader阶段1介绍
6.2.2Boot Loader阶段2介绍
6.2.3关于串口终端
6.3UBoot简介
6.3.1认识UBoot
6.3.2UBoot特点
6.3.3UBoot代码结构分析
6.4vivi简介
6.4.1认识vivi
6.4.2vivi代码导读
小结
进一步探索
第7章ARMLinux内核
7.1ARMLinux内核简介
7.1.1ARMLinux内核和普通Linux内核的区别
7.1.2ARMLinux的版本控制
7.1.3ARMLinux的代码结构
7.2ARMLinux内存管理
7.2.1影响内存管理的两个方面
7.2.2ARMLinux的存储机制
7.2.3虚拟内存
7.3ARMLinux进程管理和调度
7.3.1进程的表示和生命周期
7.3.2Linux进程的创建、执行和销毁
7.3.3Linux进程的调度
7.4ARMLinux模块机制
7.4.1Linux模块概述
7.4.2模块代码结构
7.4.3模块的加载
7.4.4模块的卸载
7.4.5版本依赖
7.5ARMLinux中断管理
7.6ARMLinux系统调用
7.7ARMLinux系统启动和初始化
7.7.1使用Boot Loader将内核映像载入
7.7.2内核数据结构初始化——内核引导第一部分
7.7.3外设初始化——内核引导第二部分
7.7.4init进程和inittab脚本
7.7.5rc启动脚本
7.7.6Shell的启动
小结
进一步探索
第8章文件系统
8.1嵌入式文件系统简介
8.1.1Linux文件系统简介
8.1.2嵌入式文件系统简介
8.2嵌入式Linux文件系统框架
8.3JFFS2嵌入式文件系统
8.3.1目录节点的定义
8.3.2数据节点
8.3.3可靠性支持
8.3.4内存使用
8.3.5垃圾收集
8.3.6写平衡
8.3.7JFFS2的不足之处
8.3.8JFFS3简介
8.4根文件系统
8.4.1什么是根文件系统
8.4.2建立JFFS2根文件系统
小结
进一步探索
第9章设备驱动程序设计基础
9.1Linux设备驱动程序简介
9.1.1设备的分类
9.1.2设备文件
9.1.3主设备号和次设备号
9.1.4Linux设备驱动代码的分布
9.1.5Linux设备驱动程序的特点
9.2设备驱动程序结构
9.2.1驱动程序的注册与注销
9.2.2设备的打开与释放
9.2.3设备的读写操作
9.2.4设备的控制操作
9.2.5设备的轮询和中断处理
9.3Linux内核设备模型
9.3.1设备模型建立的目的
9.3.2sysfs——设备拓扑结构的文件系统表现
9.3.3驱动模型和sysfs
9.3.4kobject
9.3.5platform总线
9.4同步机制
9.4.1同步锁
9.4.2信号量
9.4.3读写信号量
9.4.4原子操作
9.4.5完成事件
9.4.6时间
9.5内存映射和管理
9.5.1物理地址映射到虚拟地址
9.5.2内核空间映射到用户空间
9.6工作队列
9.7异步I/O
9.8DMA
9.8.1DMA数据传输
9.8.2DMA定义
9.8.3DMA映射
小结
进一步探索
第10章字符设备和驱动程序设计
10.1字符设备驱动框架
10.2字符设备驱动开发
10.2.1设备号
10.2.2关键数据结构
10.2.3字符设备注册和注销
10.3GPIO驱动概述
10.4串行总线概述
10.4.1SPI总线
10.4.2I2C总线
10.4.3SMBus总线
10.5I2C总线驱动开发
10.5.1I2C驱动架构
10.5.2关键数据结构
10.5.3I2C核心
10.5.4I2C总线驱动
10.5.5I2C设备驱动
小结
进一步探索
第11章块设备和驱动程序设计
11.1块设备驱动程序设计概要
11.1.1块设备的数据交换方式
11.1.2块设备读写请求
11.2Linux块设备驱动相关数据结构与函数
11.2.1gendisk结构
11.2.2request结构
11.2.3request_queue队列
11.2.4bio结构
11.3块设备的注册与注销
11.4块设备初始化与卸载
11.5块设备操作
11.6请求处理
11.7MMC卡驱动
11.7.1MMC/SD芯片介绍
11.7.2MMC/SD卡驱动结构
11.7.3MMC卡块设备驱动分析
11.7.4HSMCI接口驱动设计分析
小结
进一步探索
第12章网络设备驱动程序开发
12.1以太网基础知识
12.1.1CSMA/CD协议
12.1.2以太网帧结构
12.1.3嵌入式系统中常用的络协议
12.2嵌入式网络设备驱动开发概述
12.3网络设备驱动基本数据结构
12.3.1net_device数据结构
12.3.2sk_buffer数据结构
12.4网络设备初始化
12.5打开和关闭接口
12.6数据接收与发送
12.7查看状态与参数设置
12.8AT91SAM9G45网卡驱动
12.8.1EMAC模块简介
12.8.2模块图
12.8.3功能描述
12.8.4寄存器描述
12.8.5AT91SAM9G45芯片EMAC控制器驱动分析
小结
进一步探索
第13章嵌入式GUI及应用程序设计
13.1嵌入式GUI设计概述
13.1.1嵌入式GUI简介
13.1.2嵌入式GUI设计需求
13.1.3嵌入式GUI设计原则
13.1.4主流嵌入式GUI简介
13.2嵌入式GUI体系结构设计
13.2.1嵌入式GUI体系结构
13.2.2抽象层
13.2.3核心层
13.2.4接口层
13.3基于主流GUI的应用程序设计
13.3.1MiniGUI开发环境搭建
13.3.2基于MiniGUI的应用程序设计
13.3.3Android开发环境搭建
13.3.4基于Android的应用程序设计
小结
进一步探索
下篇实验部分
第1章实验基础
第2章开发环境建立
第3章内核和模块构建
第4章文件系统构建
第5章调试技术演练
第6章字符设备和驱动程序设计
第7章块设备驱动程序设计
第8章网络设备驱动程序设计
第9章MiniGUI应用设计
第10章Android应用设计
前言
转眼之间,国家级精品课教材、普通高等教育“十一五”国家级规划教材的《嵌入式系统原理与设计》正式发行有5年多的时间了。感谢各位读者的关注及厚爱,使得本书印刷了6次,被几十所高校选作指定教材,并被多个高校图书馆馆藏。从众多兄弟院校课程教学反馈意见来看,本书对“嵌入式系统”及相关课程教学起到了积极作用。同时,我们在互联网上也倾听了众多读者的反馈,对他们提出的宝贵的建议与意见表示诚挚的谢意。根据近几年作者在嵌入式系统及相关专业课程的一线教学实践的经验积累,以及对飞速发展的各种嵌入式系统技术的跟踪和学习,结合读者的建议和意见,决定对本书进行修订后再版发行。再版中,主要对嵌入式系统原理部分做了调整,结构和内容方面调整如下:(1) 第1章“嵌入式系统概述”在内容方面做了更新。(2) 将原第2章“ARM处理器和架构”和原第3章“ARM9指令集和汇编”合并成第2章“ARM处理器和指令集”,对处理器架构介绍方面进行缩减,使该章内容更为紧凑、实用。(3) 将原第4章“嵌入式Linux操作系统”调整为第3章,并在内容上做了更新。(4) 新增加一章“嵌入式软件编程技术”作为第4章,介绍嵌入式编程基础,并在此基础上深入讲解嵌入式汇编编程技术、嵌入式高级编程技术和汇编语言与高级语言混合编程技术,以便读者在做后面章节内容设计时有更好的编程基础。(5) 将原第9章“开发环境和调试技术”调整为第5章,并在内容上做了更新,使读者学习完编程技术后,接着学习嵌入式系统开发环境搭建和调试技术,顺序上更科学。(6) 将原第5章“Boot Loader技术”调整为第6章,并在内容上做了更新。(7) 将原第6章“ARM——Linux内核”调整为第7章,并在内容上做了更新。(8) 将原第7章“文件系统”调整为第8章,并在内容上做了更新。(9) 将原第8章“设备驱动程序设计基础”调整为第9章,并在内容上做了更新。(10) 第10章“字符设备驱动程序设计”、第11章“块设备驱动程序设计”和第12章“网络设备驱动程序开发”在内容方面做了更新。(11) 将原第13章“MiniGUI”和原第14章“Android嵌入式系统及应用开发”合并为第13章“嵌入式GUI及应用程序设计”,并对该章进行重写,从嵌入式GUI设计的基本知识入手,然后分析嵌入式GUI的典型体系结构设计,最后介绍基于两种主流GUI的应用程序设计,结构更为紧凑,内容更为实用。本次再版,在浙江大学陈文智教授等提出的“基于软硬件贯通和分级分层次的系统能力培养创新体系”的指导下,由王总辉编写和整理,最后由陈文智和王总辉定稿。本书的编写和再版工作是在国家教委的指导下进行的,并得到了国内外同行和同事们给予的真切关心、指导和热情帮助,在此向各级机关以及所有关心、支持本书出版工作的朋友表示衷心的感谢。在本书的编写和再版过程中,我们已尽全力保证本书内容的正确性,但由于时间匆忙,且作者自身水平有限,仍然可能有错误存在。无论如何,请读者不吝赐教,以便我们在改版或再版的时候及时纠正补充。希望本书能一如第1版,继续为嵌入式系统学习和开发的读者提供力所能及的帮助。
编者2016年秋于浙江大学
嵌入式系统的开发环境与通用计算机大不相同,从硬件资源上说它有很大的局限性,比如存储空间小,处理器频率低,甚至没有键盘、鼠标等设备,这也就限制了已有的开发调试工具(比如GNU软件)在嵌入式系统上的使用。另外,硬件资源的局限性会给嵌入式软件开发和调试带来一定的约束,比如内存使用。因此,开发人员经过长时间的探索,提出了一种方便和有效的开发和调试模式,即宿主机目标板交叉开发模式。本章将首先介绍交叉开发模式的主要原理,并分别从宿主机和目标板两个方面简单介绍基础环境的搭建。接着介绍交叉编译工具链。最后介绍几种常用的调试技术,包括gdb本地调试、远程调试、内核调试和网络调试等。本章介绍的概念和工具都需要熟悉掌握,这是今后嵌入式系统开发不可或缺的技能。通过本章的学习,读者可以获得以下知识点。(1) 交叉开发模式; (2) 交叉编译工具链构建; (3) gdb本地调试技术; (4) 远程调试技术; (5) 内核调试技术; (6) 网络调试技术。5.1交叉开发模式概述嵌入式系统在硬件上的局限性,造成通用计算机的集成开发环境很难原封不动地移植到嵌入式平台。嵌入式系统的存储空间小,不能够安装完整的操作系统; 处理器频率低,无法进行大量的编译运算等工作。这些都使得直接在嵌入式系统平台上进行开发设计困难重重,开发人员不得不采用另外一种模式,即宿主机目标板交叉开发模式。宿主机目标板交叉开发模式,主要由两个部分组成: 一是宿主机,就是平时使用的桌面计算机; 二是目标板,指的是嵌入式开发板。通过交叉开发环境的方式,在宿主机上利用已有的成熟的开发工具,专门针对目标板定制一套系统,包括引导程序、内核和文件系统,然后下载到目标板上运行。而以后嵌入式应用程序的开发,都可以在宿主机上编辑,并通过交叉编译工具编译出能够在目标板上运行的程序,然后下载到目标板上测试执行,最后利用宿主机上的调试工具对目标板上运行的程序进行远程调试。目前许多主流的操作系统都包含非常丰富的开发工具,并在许多领域广泛使用。其中比较著名的有Linux操作系统,它是一款非常优秀的开源操作系统,并且绝大多数基于Linux内核的操作系统使用了大量的GNU软件,包括shell、glibc、gcc、gdb等,还有许多其他功能强大的程序,例如Vim、Emacs。开源系统和软件可以自由下载使用,而且越来越多的人致力于开发Linux系统和软件,这使得Linux系统越来越稳定,应用也越来越广泛。因此,大多数嵌入式系统都选择Linux作为主要的操作系统。交叉开发环境的模式使得开发人员可以使用熟悉的开发工具,而不需要重新学习掌握另外的工具,就可以在嵌入式平台上进行开发设计,这样极大地提高了嵌入式系统的开发效率。通常,宿主机和目标板的连接方式有4种,分别是串口、以太网接口、USB接口和JTAG接口。这4种连接方式各有好坏,需要在不同的场合正确地使用才能发挥它们的最大功用。串口可以当作终端使用,利用串口给目标板发送命令,同时也可以接收目标板返回的信息并显示。宿主机可以通过串口往目标板传送文件; 目标板可以把程序运行的结果返回并显示。串口驱动程序的实现相对比较简单,缺点是传输速度慢,并不适用于传输大量数据的场合。以太网是当今局域网采用的最通用的通信协议标准。它使用简单,配置灵活,支持广泛,传输速率快,安全可靠,缺点是网络驱动的实现比较复杂。USB是Universal Serial Bus(通用串行总线)的缩写,现已成为PC的标准,很多基于USB标准的设备被广泛使用。它是一种快速、灵活的总线接口,与其他通信接口相比,USB接口的特点是易于使用。另外,USB还支持热插拔,无须用户自己配置,系统会自动搜索驱动并安装。然而USB是典型的主从结构,两端分别需要不同的驱动程序。JTAG(Joint Test Action Group,联合测试行动小组)是一种国际标准测试协议,主要用于芯片内部测试及对系统进行仿真、调试。在嵌入式系统领域,几乎所有的处理器都支持JTAG,调试器的单步调试和断点都需要和JTAG交涉。另外,还可以使用JTAG将程序烧写到目标板上。5.2宿主机环境宿主机和目标板使用不同的平台,因此交叉开发模式属于跨平台开发。开发人员利用宿主机上的开发工具,开发设计能够在目标板上运行的应用程序。由于目标板的实际操作系统不提供编译器或者开发环境不完整,甚至没有操作系统,通常采用交叉编译的方式产生目标代码。一般情况下,宿主机的性能要远超出目标板,因此交叉编译也可以节约开发时间。交叉编译采用的工具链通常和目标板运行的操作系统紧密相关。另外,目标板需要通过通信接口向宿主机提出请求,比如IP分配、文件传输等,这就需要宿主机提供相应的服务,比如DHCP、TFTP等。5.2.1串口终端上文曾提到,串口并不适用于传输大量数据的场合,而是可以作为终端来使用。串口终端主要用来控制管理嵌入式系统,例如管理Boot Loader、输入命令等,这样就可以免去额外的键盘、鼠标和显示器等。串口终端的使用非常广泛,因此很多操作系统上面都已经集成了超级终端工具,比如Windows下面的超级终端和Linux下面的Minicom,都是用得比较普遍的串口终端工具。与拥有图形界面(Graphic User Interface,GUI)的Windows超级终端不同,Linux下的Minicom采用的是命令行界面(Command User Interface,CUI)。Minicom的优点是操作简单方便,配置都是以菜单的形式进行选择。5.2.2BOOTP在一台连接到TCP/IP网络计算机能够有效地同其他计算机通信之前,它必须知道自己的IP地址。通用计算机可以从硬盘中读取IP信息,但是对一些无盘的嵌入式设备来说,就没办法办到了。因此,它们只能通过网络上的其他计算机来提供IP地址和其他一些必要的信息,为此,开发人员提出了一种新的协议,即BOOTP。引导协议(Bootstrap Protocol,BOOTP)是一种基于TCP/IP的协议,它最初在RFC951中定义,如今在通用计算机上广泛使用的DHCP就是从BOOTP扩展而来。BOOTP使用TCP/IP网络协议中的UDP 67/68两个通信端口。BOOTP主要是用于无盘客户机从服务器得到自己的IP地址、服务器的IP地址、启动映像文件名、网关信息等。这个过程处理如下。第一步,在主机平台运行BOOTP服务的情况下,目标板由Boot Loader启动BOOTP,此时目标板还没有IP地址,它就用广播形式以IP地址0.0.0.0向网络中发出IP地址查询的请求,这个请求帧中包含客户机的网卡MAC地址。第二步,主机平台上的BOOTP服务器接收到的这个请求帧,根据帧中的MAC地址在Bootptab启动数据库中查找这个MAC的记录,如果没有此MAC的记录则不响应这个请求; 如果有就将FOUND帧发送回目标板。FOUND帧中包含的主要信息有目标板的IP地址、服务器的IP地址、硬件类型、网关IP地址、目标板MAC地址和启动映像文件名。第三步,目标板就根据FOUND帧中的信息通过TFTP服务器下载启动映像文件。5.2.3TFTPTFTP的全称是Trivial File Transfer Protocol,可以翻译为“简单文件传输协议”,它是TCP/IP协议族中的一个在客户端和服务端之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。FTP想必读者非常熟悉,TFTP可以看成一个简化了的FTP。它们之间主要的区别是,TFTP没有用户权限管理的功能,也就是说TFTP不需要认证客户端的权限,这样远程启动的目标板在启动一个完整的操作系统之前就可以通过TFTP下载启动映像文件,而不需要证明自己是合法的用户。这样一来,TFTP服务就存在着比较大的安全隐患,现在黑客和网络病毒也经常用TFTP服务来传输文件。所以TFTP在安装时一定要设立一个单独的目录作为TFTP服务的根目录,作为下载启动映像文件的目录,TFTP服务只能访问这个目录。另外还可以设置TFTP服务为只能下载不能上传等,以减少安全隐患。5.2.4交叉编译交叉编译就是在一个架构的机器下编译另一个架构的目标文件。目标文件在不同架构间由于采用的CPU指令集不同等原因不能通用。比如x86架构的程序不能运行于ARM架构的XSBase255目标板。而且通常在一个架构下,会有多个操作系统。不同的操作系统会使用不同的目标文件格式,所以采用何种交叉编译器产生何种格式的目标文件还要取决于目标板的操作系统。这里讲的交叉编译就是在x86架构的宿主机上生成适用于ARM架构的ELF格式的可执行代码。如果没有可用的二进制交叉编译器,就需要手工编译交叉编译器。在第5.4节中将会具体介绍如何构建交叉编译工具链。5.3目标板环境5.3.1JTAG接口简介
作为硬件测试手段,JTAG的功能与CPU状态无关,可驱动设备的所有外部引脚并读入数据,而且在设备内部夺取外部的连接点(与通往外部的各个pin脚一一连接)。各个cell为了形成Serial Shift Register(Boundary Scan Register)而相连。整体的接口由5个pin脚来控制(TDI,TMS,TCK,nTRST,TDO)。其功能包括: 测试线路连线和端子的连接状态; 测试设备间的连接状态; 进行Flash memory烧写等。5.3.2Boot Loader简介Boot Loader是系统加电后运行的第一段代码。在PC中引导程序一般由BIOS和位于MBR的操作系统Boot Loader(如LILO或GRUB)组成。然而在嵌入式系统中通常没有BIOS这样的固件程序,因此整个系统的加载启动任务就完全由Boot Loader来完成。简单说来,Boot Loader就是操作系统内核运行前执行的一段小程序,完成初始化硬件设备、创建内核需要的信息等工作,最后调用操作系统内核。因此Boot Loader的实现对硬件的依赖非常强,不同的体系结构、不同的嵌入式板级设备配置都会对Boot Loader有不同的需求。通常情况下,Boot Loader通过串口与宿主机进行文件传输,但串口传输的速度是有限的,因此通过以太网连接并借助TFTP来下载文件是一个更好的选择。5.4交叉编译工具链嵌入式系统由于硬件资源上的局限性,没有充足的存储空间和运算能力,而一般而言,编译器需要很大的存储空间,并需要很强的CPU处理运算能力。因此在交叉开发环境下需要借助宿主机的编译环境。编译的过程就是把用高级语言编写的应用程序转化成运行该程序的CPU所能识别的机器代码。由于不同的架构有不同的指令集,因此不同的CPU需要不同的编译器。一个平台上编译的代码不能直接在另外一个平台上执行。因此,在跨平台的开发中往往需要交叉编译工具链。通过交叉编译工具链,可以在x86平台上编译出能够在ARM平台上运行的程序,编译得到的程序在PC上不能运行,而只能在ARM平台上执行。这种方法充分利用了PC的丰富资源和优秀的集成开发环境,从而弥补了嵌入式系统开发的不足。相对于交叉编译,平时做的编译称为本地编译。交叉编译工具链是一个由编译器、链接器和解释器组成的集成开发环境。和本地编译类似,交叉编译的过程也是由编译、链接等阶段组成的,源程序通过交叉编译器编译成目标模块,并由交叉链接器加载库最后链接成可在目标平台上执行的程序代码。交叉编译的主要过程如图51所示。
图51交叉编译过程
5.4.1交叉编译的构建交叉编译的过程其实并不复杂,但是要完成交叉编译工具链的制作却是比较困难的。网上有许多交叉编译的构建方法可以提供参考。在制作工具链之前,首先要明确目标平台,比如嵌入式开发一般是在ARM平台下,这样才能选择正确的交叉编译工具,比如armlinuxgcc。通常交叉编译的构建有以下三种方法,它们由难到易分别如下。1. 从头编译这种方法是最为困难的,它分别编译和安装交叉编译工具链所需要的各种库和源代码,最终生成交叉编译工具链。在编译过程中,有许多依赖关系和配置选项,往往会因此而出现各种编译错误。推荐想要深入学习交叉编译工具链的读者可以尝试这种方法,可以加深对整个过程的理解。2. 脚本编译通过网上专门提供的Crosstool脚本工具,选择合适的平台脚本来一次性地编译生成交叉编译工具链。与方法1相比,这种方法节省了许多配置,相对简单了许多。3. 下载使用如果只想使用交叉编译工具链,而不想花太多时间制作它们,推荐去网上直接下载已经制作好的交叉编译工具链。这种方法最为简单,但缺点是不够灵活,不一定能够满足所有人的开发需求。在实际的开发过程中,读者可以根据自己的需要选用以上任意一种方法来构建交叉编译工具链。5.4.2相关工具交叉编译工具链主要包括: (1) 标准库(2) 编译器(3) 链接器(4) 汇编器(5) 调试器以上功能主要由glibc、gcc、binutils和gdb 4个软件包提供,gdb作为调试工具将在5.5节重点介绍。1. glibcglibc全称为GNU C Library,它是一种按照LGPL许可协议发布的,公开源代码的,可以免费从网络下载的C的编译程序。glibc最初是自由软件基金会为其GNU操作系统所写,但目前最主要的应用是配合Linux内核,成为GNU/Linux操作系统一个重要的组成部分。glibc是Linux系统中最底层的API,几乎其他任何运行库都会直接或间接地依赖于glibc。glibc除了封装系统调用之外,还提供一些基本的功能,例如open、malloc、printf、exit等。2. gccgcc是GNU Compiler Collection的缩写,它是GNU项目中最具有代表性的作品,gcc支持不同的编程语言,它被目前许多UNIX/Linux系统作为默认的标准编译器。gcc已经被移植到多种处理器架构上,并且在商业、专利和开源软件开发环境中广泛使用。gcc同样适用于嵌入式系统平台,比如Symbian、AMCC和Freescale Power等。gcc最初命名为GNU C Compiler,因为它仅处理C语言。1987年GCC 1.0发布,同年12月它开始支持编译C 语言。后来,gcc支持越来越多的编译语言,包括FORTRAN、Pascal、ObjeciveC、Java和Ada等,而gcc的意思也不仅仅是GNU C Compiler了,而变成了更加强大的GNU Compiler Collection。gcc是一个交叉平台的编译器,目前支持几乎所有主流处理器平台,它可以将源文件编译成在指定平台硬件上可执行的目标代码。gcc不仅功能非常强大,结构也异常灵活,便携性与跨平台支持特性是gcc的显著优点。目前最新的gcc版本是4.4.3,但是在选择gcc的版本时并不是越新越好。新版本虽然添加了一些新特性,但是同样也会带来许多潜在的Bug。由于发布的时间短,并没有广泛推广使用。因此在实际开发过程中,尽量要选择稳定的版本。gcc编译过程一般分成4个阶段,分别是预处理、编译、汇编和链接。预处理阶段,gcc首先调用cpp命令,在这个过程主要是对源文件中的包含文件和预编译语句进行分析并展开。接着进入编译阶段,用cc命令编译源文件生成目标文件。汇编过程是针对汇编语言的步骤,这一步通过调用as命令生成目标文件。最后就是链接,它由ld命令来完成。下面通过一个例子来讲述gcc的编译流程,同时介绍一些常用的gcc命令选项。
#include “hello.h”
int main()
{
printf(“%s”, HELLO);
return 0;
}
预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。预处理语句是以#开头的代码行。gcc中可以使用“E”选项,使得在预处理阶段停止,默认输出预处理的结果到标准输出。如果源代码不需要预处理,则什么事都不会做。
# gcc -E hello.c -o hello.i
“o”选项指定输出文件名,这条语句执行的结果如下。
# 1 “hello.c”
# 1 “”
# 1 “”
# 1 “hello.c”
# 1 “hello.h” 1
# 2 “hello.c” 2
int main()
{
printf(“%s”, “Hello, world!\n”);
return 0;
}
可见,在预处理阶段,include语句和宏都在相应的地方展开。编译阶段主要负责检查语法格式,如果无误,则把代码翻译成汇编文件,汇编文件一般后缀为.s。同选项“E”类似,gcc命令的“S”选项告诉编译器在编译生成汇编代码后停止,不进行后续的汇编工作。
# gcc -S hello.i -o hello.s
汇编阶段就是把汇编代码转换成目标文件,这里使用选项“c”。
# gcc -c hello.s -o hello.o
最后通过下面的命令完成最后的链接工作,生成可执行文件,执行得到最后的结果。
# gcc hello.o -o hello
# ./hello
Hello, world!
gcc是一个非常强大的编译工具,拥有众多的命令选项,其中包括常规选项、预警和错误选项、优化选项和体系相关选项。合理地使用gcc的各种选项,能有效地提高代码质量和编译效率。一般来说,在实际应用中前两者用到的比较多,后面两种选项在项目工程规模比较大的时候会用到。gcc编译的时候提供的警告和错误信息,可以帮助程序员改进代码,增加程序的健壮性。常规选项在平时使用中也会经常碰到,如表51所示,部分已经在前面提到过。
表51gcc常用编译选项
选项含义
E只进行预编译,不做其他处理S只进行编译不汇编,生成后缀为.s的汇编文件c只进行编译不链接,生成后缀为.o的目标文件o file指定输出文件保存到fileg创建用于gdb的符号表和调试信息v显示编译器命令行信息和版本信息Idir添加头文件搜索路径dirLdir添加库文件搜索路径dirllibrary链接库文件library
这些选项需要在实际应用中灵活使用。例如,编写一个需要使用libm库的程序,直接按照前面的编译过程会出错,需要使用“lm”指明链接libm.so库。指定“l”选项的时候,gcc会去系统的默认库目录查找,如果找不到就会出错,这时候就可以使用“Ldir”添加库文件搜索路径。同样道理,在头文件目录不在系统指定的默认目录的时候,也可以使用“Idir”添加额外的搜索路径。本书只是介绍一些基本的gcc命令选项,更多的选项可以在使用的时候查看gcc帮助文档。3. binutilsbinutils是一组开发工具包,包括链接器、汇编器和其他用于目标文件和档案的工具。binutils中的不少工具和gcc相类似。binutils工具包是在嵌入式系统开发中必须掌握的,主要包括以下部分。addr2line是用来将程序地址转换成其所对应的程序源文件及所对应的代码行号,当然也可以得到所对应的函数。前提是在编译的时候使用了g的选项,即在目标代码中加入调试信息。ar是用来管理归档文件,例如创建、修改、提取归档文件等。归档文件是一个包含多个文件的单一文件,有时也被称为库文件,其结构保证了可以从中检索并提取原始的被包含的文件。在嵌入式系统开发中, ar主要用于管理静态库。as主要用来编译gcc输出的汇编文件,生成的目标文件由链接器ld链接。ld是GNU提供的链接器,主要功能是将目标文件和库文件结合在一起重定位数据并链接符号引用。nm用来列出目标文件中的符号清单,包括变量和函数等。如果没有指定目标文件,则默认使用a.out。objdump可以用来查看目标程序的信息,可以通过选项控制显示那些特定信息,也可以用来对目标程序进行反汇编。程序是由多个段组成的,比如.text段是用来存放代码,.data段用来存放已经初始化过的数据,.bss用来存放尚未初始化过的数据等。在嵌入式系统的开发过程中,也可以用它查看执行文件或库文件的信息。比如查看其中的某个段在程序运行时的起始地址是什么。objcopy可以进行目标文件的格式转换。它使用GNU BFD库进行读/写目标文件。使用BFD,objcopy就能将原始的目标文件转化为不同格式的目标文件。ranlib可以用来生成归档文件索引,这样使得存取归档文件中被包含文件的速度更快,它的功能和“ar s”是一样的。readelf用来显示ELF格式目标文件的信息,可通过参数选项来控制显示哪些特定信息。ELF格式是UNIX/Linux平台上应用最为广泛的二进制文件标准之一。5.5gdb调试器调试是应用程序开发过程中必不可少的环节之一。gdb是GNU C自带的调试工具。它可以使得程序的开发者了解到程序在运行时的细节,从而能够很好地除去程序的错误,达到调试的目的。英文Debug的原意就是“除虫”,而gdb的全称就是Gnu DeBugger。gdb是一款功能非常强大的调试器,既支持多种硬件平台,也支持多种编程语言,目前gdb支持的调试语言有C/C 、Java、FORTRAN、Modula2等多种语言。gdb不仅用于本地调试,还可以用于远程调试,非常适合嵌入式系统开发使用。使用gdb可以完成下面这些任务。(1) 运行程序,可以给程序加上所需的任何调试条件; (2) 在给定的条件下让程序停止; (3) 检查程序停止时的运行状态; (4) 通过改变一些数据,可以更快地改正程序的错误。gdb提供了大量的命令,用来完成程序的调试; gdb本身只是基于命令行界面的程序,工作在终端模式。而xxgdb在gdb的基础上还实现了图形前端,它的调试界面友好,比较有名的gdb图形前端工具还有DDD等。在使用gdb调试程序之前,必须使用“g”编译选项编译源文件,从而在目标文件中加入调试信息,这些信息可以被gdb等调试工具利用。可以在Linux终端下输入“gdb”,简单地启动gdb调试器。
# gdb
GNU gdb (gdb) 7.0-ubuntu
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3 : GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type “show copying”
and “show warranty” for details.
This gdb was configured as “i486-linux-gnu”.
…
(gdb)
启动后,gdb会显示版本以及平台信息,从上面的输出可以看出,这里采用的是Ubuntu平台下gdb 7.0版本,编译时配置的目标平台是i486linuxgnu。最下面是以(gdb)开头的提示符,表示可以在后面输入gdb调试相关的命令。如果不喜欢启动gdb时显示版本及平台信息,可以加上q选项指定gdb以安静模式启动。gdb启动的时候可以显式地指定需要调试的可执行程序,命令语法如下。
gdb [options] [executable-file [core-file or process-id]]
gdb [options] –args executable-file [inferior-arguments …]
假如需要调试的可执行程序名为program,可以使用以下方式启动gdb。
# gdb program
当调试的程序需要指定参数的时候,可以在gdb启动的时候打开args选项,在可执行文件名后提供需要的参数。
# gdb –args program 10 20
如果程序在执行过程中意外崩溃,操作系统会把程序崩溃时的内存内容存储到core dump文件中,该文件可以配合gdb方便地对可执行程序进行调试。假如program崩溃后生成的core dump文件为program.core,gdb可以通过指定core dump文件进行调试。
# gdb program program.core
另外一种情况是被调试的可执行程序是一个系统服务,那么可以在启动的时候指定此服务的进程ID号。注意,program必须在系统PATH中可以找到。
# gdb program 1234
如果不想在gdb启动的时候指定调试的可执行程序,也可以在进入gdb后,使用file命令载入调试程序。file命令后跟着的参数就是要被调试的可执行程序,gdb在载入调试程序后读取调试信息,例如符号表等。
(gdb) file program
Reading symbols from program…done.
载入调试程序后,就可以使用gdb提供的命令进行调试了。gdb的强大功能足以跟Visual Studio相比,它拥有非常多的调试命令。按照功能特点分类,主要分成别名(alias)、断点(breakpoints)、数据(data)、文件(file)、程序执行(running)和堆栈(stack)等几个部分,每类命令都包含功能相似的一组命令集合。更多的类别可以在gdb提示符后输入help查看。在gdb中,help命令是非常有用的一个命令,它可以用来查看某个命令或者某类命令的用法。例如,输入help breakpoints可以查看有关断点的所有命令。
(gdb) help breakpoints
Making program stop at certain points.
List of commands:
awatch — Set a watchpoint for an expression
break — Set breakpoint at specified line or function
…
trace — Set a tracepoint at specified line or function
watch — Set a watchpoint for an expression
Type “help” followed by command name for full documentation.
Type “apropos word” to search for commands related to “word”.
在help后面指定命令类别可以打印出此类调试命令的子命令集合,同样地,在help命令后面指定特定的某个子命令可以得到这个子命令的具体用法。例如,help break。
(gdb) help break
Set breakpoint at specified line or function.
break [LOCATION] [thread THREADNUM] [if CONDITION]
LOCATION may be a line number, function name, or “*” and an address.
If a line number is specified, break at start of code for that line.
If a function is specified, break at start of code for that function.
If an address is specified, break at that exact address.
With no LOCATION, uses current execution address of selected stack frame.
This is useful for breaking on return to a stack frame.
THREADNUM is the number from “info threads”.
CONDITION is a boolean expression.
通过help命令,可以非常方便地知道gdb调试命令的用法,上面的help break的输出结果表示,break断点命令是在指定行或者指定函数或者以“*”开头的指定地址处设置断点。如果没有显式地指定断点位置,则在选择的栈帧中即将执行的地址上设置一个断点。另外,还可以添加条件判断,在条件满足时才断点,这在调试的时候是非常有用的。gdb不仅功能强大,并且和用户交互的设计也是非常人性化的。gdb的命令输入采用和Bash类似的命令自动补全功能。当用户输入一个命令的起始部分时,可以使用Tab键补全。如果符合的命令不止一个,需要连续按两次Tab键。有时候要记住这么多命令是非常困难的,使用自动补全可以更加灵活地使用gdb命令,而不需要记住每个命令的完整形式。另外一个技巧是,可以使用命令缩写来代替输入完整的命令,比如break命令可以直接输入“b”,在后台gdb会自动补全成break命令。如果使用过Vim的读者肯定知道,在Vim中可以使用!cmd调用外部shell命令,或者输入“:shell”临时启动shell外壳,外壳退出后(执行完exit命令)返回到Vim界面。同样地在gdb中除了使用本身的调试命令外,也可以执行shell下的命令。它是通过gdb提供的shell命令来实现的,可以使用help shell查看shell命令的帮助信息。
(gdb) help shell
Execute the rest of the line as a shell command.
With no arguments, run an inferior shell.
使用shell命令的方法有两种,一种是在shell命令后面跟着shell下要执行的命令,比如要使用date命令查看系统时间。
(gdb) shell date
Thu Mar 25 10:16:44 CST 2010
另外一种方法是像Vim一样临时启动shell外壳,在执行完命令后输入“exit”命令返回gdb界面。在gdb调试的过程中,免不了需要用shell同系统交互,使用这种方法就节省了退出再重新启动gdb的烦琐步骤。值得一提的是,可以不必通过shell就可以在gdb下执行make命令,该命令的用法同shell下是一模一样的。gdb也有命令历史功能,默认是关闭的,可以通过执行以下命令,打开历史功能。
(gdb) set history filename cmdhistory
(gdb) set history save on
(gdb) set history size 100
这三条语句的作用是指定命令历史文件名,开启命令历史功能,并且指定历史记录的命令条数。启用命令历史功能后,就可以使用方向键回滚以前执行过的命令。调试过程结束后,可以使用quit(缩写q)退出gdb界面,或者也可以使用Ctrl D键退出gdb。通过以上的介绍,相信读者对gdb的使用已经有了初步的了解,这里只是引导读者去了解如何使用gdb,有关gdb的调试命令可以参考gdb的相关帮助文档,本书下篇也有gdb调试的实验内容。5.6远 程 调 试5.6.1远程调试原理
在桌面操作系统上,调试器和被调试的程序往往是运行在同一台机器上的两个进程,调试器需要通过操作系统专门提供的调试接口(比如早期UNIX系统的ptrace调用和现在的进程文件系统)来控制和访问被调试进程,这种调试称为本地调试。而嵌入式操作系统往往不具备使用本地调试的能力,原因大多归结于自身硬件和软件上的局限性,比如嵌入式系统自身的资源有限,内存小,输入和输出设备不能用于调试,又或者嵌入式系统通常无文件系统,尤其是在内核调试时还不支持文件系统。因此,在嵌入式操作系统上,为了向系统开发人员提供灵活方便的调试界面,调试器往往运行在宿主机上,而被调试的程序则运行在目标板上,相对于本地调试,这种方法被称为远程调试。这就带来以下问题: 调试器与被调试程序如何通信; 被调试程序产生异常如何及时通知调试器; 调试器如何控制和访问被调试程序; 调试器如何识别有关被调试程序的多任务信息并控制某一特定任务; 调试器如何处理某些与目标硬件平台相关的信息(如目标平台的寄存器信息、机器代码的反汇编等)。要解决以上问题,需要在目标操作系统和宿主机调试器内分别添加一些功能模块,然后二者互通信息调试,这种方案称为插桩(Stub)。使用插桩方案,可以解决以上所提到的几个问题。1. 调试器与被调试程序的通信宿主机调试器与目标板被调试程序通过指定通信端口(串口、网卡、并口)并遵循远程调试协议进行通信。2. 被调试程序产生异常及时通知调试器目标板被调试程序产生的所有异常处理转发给通信模块,通知宿主机调试器当前的异常代码; 宿主机调试器据此向用户显示被调试程序产生了哪一类异常。3. 调试器控制、访问被调试程序宿主机调试器的控制访问请求,实际上都将转换成对目标板被调试程序的地址空间或目标操作系统的某些寄存器的访问,目标操作系统可以直接处理这样的请求。4. 调试器识别有关被调试程序的多任务信息并控制某一特定任务由目标操作系统提供专门接口。目标操作系统根据宿主机调试器发送的多任务请求,调用该接口提供相应信息或对某一特定任务进行控制,并返回调试信息给宿主机调试器。5. 调试器处理与目标硬件平台相关的信息第2条所述调试器应能根据异常号识别目标平台产生异常的类型也属于这一范畴,这类工作完全可以由调试器独立完成。支持多种目标平台正是gdb的一大特色。综上所述,插桩方案的实现需要目标操作系统提供支持远程调试协议的通信模块(如串口驱动)和多任务调试接口,并且还需要改写异常处理的相关部分。另外,目标操作系统还需要定义一个设置断点的函数,因为有的硬件平台并没有提供产生特定调试异常的断点指令。这些添加的模块统称为stub。运行在目标板上的被调试程序,一经初始化,在入口点会调用设置断点的函数,主动触发异常然后由异常处理程序控制,异常处理程序将会调用调试端口通信模块,监听宿主机调试器发送的调试信息。双方通信一旦建立,就可以根据远程调试协议进行调试。它的原理如图52所示。
图52远程调试原理
就目前而言,嵌入式操作系统中的远程调试方法主要分为三种: 一是用ROM Monitor调试目标板程序; 二是用kgdb调试系统内核; 三是用gdbserver调试用户空间应用程序。这三种方法之间的主要区别在于目标板调试stub的存在形式的不同,而它们的设计思路和实现过程是大致相同的。调试stub的实现和使用方式一般与硬件平台和应用场合有关,为了最好地利用特定硬件的特征,往往会设计相应的调试stub。不过,尽管远程调试具有依赖目标的特性,但还是可以创建一个高度的可移植的调试stub,使得它可以在不同的硬件平台上只需少量修改就可以做到重用。5.6.2gdb远程调试功能gdb可以调试各种程序,包括C/C 、Java、FORTRAN等高级语言和GNU所支持的所有微处理器的汇编语言。在嵌入式Linux系统中,开发人员可以在宿主机上使用gdb方便地以远程调试的方式调试目标操作系统上运行的程序。gdb远程调试功能包括单步调试程序、设置断点、查看内存等。gdb远程调试主要由宿主机gdb和目标板调试stub共同构成,两者又通过串口或TCP连接,采用的通信协议是标准的gdb远程串行协议(Remote Serial Protocol,RSP),通过这种机制实现对目标板上的系统内核和高层应用程序的控制和调试功能。gdb远程串行协议定义了宿主机gdb和被调试的目标板程序进行通信时数据包的格式。它是一种基于消息的ASCII码协议,包含内存读写、寄存器查询、程序运行等命令。调试stub作为宿主机gdb和目标板被调试程序通信的媒介,实现远程串行协议中读写内存、寄存器和stop、continue指令等。gdb源码包中提供的stub文件(*stub.c)实现了目标板端的通信协议,而宿主机端则是在remote.c文件中实现。通常情况下,可以直接使用这些子程序实现通信而不需要关注其中的细节。即使要按照需要自己实现stub文件,也可以忽略实现细节,在已有的stub文件基础上进行修改,比如sparcstub.c文件结构最清晰,便于阅读和修改。要使用gdb进行远程调试,在目标板端必须将被调试的应用程序和实现远程通信协议的调试stub链接成可执行程序。调试stub和特定的硬件平台相关,比如前面提到的sparcstub.c就是用于调试SPARC体系下的程序。除此之外,与gdb一同发布的调试stub包括以下几个。(1) i386stub.c: 用于Intel 386和兼容体系。(2) m32rstub.c: 用于Renesas M32R体系。(3) m68kstub.c: 用于Motorola 680X0体系。(4) shstub.c: 用于Renesas SH体系。当然不同的版本提供的stub文件会有所不同,具体还需要查看相关的文档。在宿主机端相对就简单得多,因为gdb已经知道如何使用远程通信协议。当所有步骤完成之后,就可以在gdb命令行输入target remote命令进行远程调试。关于远程调试将会在5.6.3节具体介绍。不过,即使如此,要成功移植stub仍然有许多困难,因此gdb又提供了另外一种远程调试方法——gdbserver。gdbserver是一种特殊的stub调试方式,它是gdb自带的用于类UNIX系统的控制程序,允许远程gdb通过target remote命令直接调试目标板上的程序,而无须将被调试程序和调试stub链接在一起。gdbserver的工作原理同gdb本地调试相似,通过将被调试程序作为其子进程,利用内核提供的代码跟踪机制(ptrace)监控被调试程序的执行,从而完成调试任务。它的调试模型如图53所示。
图53gdbserver调试模型
5.6.3使用gdbservergdbserver并不能完全代替一般的调试stub,这是因为gdbserver调试方式要求宿主机和目标板上的操作系统必须具有相同的系统调用接口。然而,由于gdbserver本身的体积小,能够在资源有限的系统上独立运行,因此非常适合于嵌入式系统开发。同时,它具有良好的可移植性,可交叉编译到不同的平台上运行,使用起来比stub方式简单得多。因此,在实际开发中经常使用gdbserver来调试用户空间的程序,比如交叉编译中,可以使用gdbserver作为调试的一种选择。要对目标板上的程序进行远程调试,首先需要连接到目标平台。gdb提供了两种连接方式: 一种是通过串口连接; 另一种是通过TCP或者UDP连接。两者都遵循标准的gdb远程串口协议。1. 连接到远程目标使用gdbserver调试方式时,在目标板端需要有一份被调试程序的拷贝,宿主机端则需要被调试程序以及其源代码文件。由于gdbserver并不处理程序符号表,符号表是由宿主机端的gdb处理的。所以如果有必要,可以使用strip工具将复制到目标板的被调试程序的符号表去掉,从而节省空间。当然,被调试程序需要使用交叉编译工具编译,并且gdbserver也需要用交叉编译工具编译到目标平台上。在宿主机gdb发起远程调试之前,需要先在目标板上启动gdbserver和要调试的程序。尤其是当使用TCP连接方式时,必须先在执行宿主机gdb的target remote命令之前启动目标板上的gdbserver,否则将无法建立远程调试连接。要使用gdbserver,必须显式地指定与gdb的通信方式、被调试程序的名称以及被调试程序需要的参数。常用的语法是:
# gdbserver comm program [ args … ]
comm可以是一个串行设备名称或者TCP主机名和端口号。比如使用参数foo.txt调试Vim,并且通过串口/dev/S1同gdb通信。
# gdbserver /dev/ttyS1 vim foo.txt
然后gdbserver被动地等待宿主机的gdb与其进行通信。若是要通过TCP方式同gdb进行通信而非串口方式,则需要使用以下的命令。
# gdbserver hostname:portname vim foo.txt
其中,参数hostname:portname的意思是指,gdbserver希望从宿主机(hostname指的是宿主机名或者其IP地址)到本地TCP端口(portname指定的端口号)建立TCP连接。端口号可以任意选择,只要它不同目标操作系统上已经被使用的任何TCP端口冲突,比如23是Telnet服务的保留端口号,建议使用大于1024的端口号。同时,此端口号必须同宿主机gdb中的target remote命令使用同一个端口号,否则调试连接是不能建立的。在某些目标板上,gdbserver也可以依附到正在运行的程序上,这主要是通过attach参数来完成的。语法是:
# gdbserver comm -attach PID
其中,PID是当前运行的进程ID号。如果有一个程序有多个映像在执行,或者程序有多个线程,在这种情况下,绝大多数版本的pidof支持s选项,这将只返回第一个进程的ID号。
# gdbserver comm –attach ‘pidof -s program’
一旦目标板上启动gdbserver并指定被调试程序之后,在宿主机端就可以通过target remote命令建立一个到目标板的连接。同样地,既可以使用串口也可以使用TCP或UDP同目标板通信。无论哪一种情况,gdb都使用同一种协议调试程序,只是通信媒介不同而已。若使用串口方式连接,它的语法如下。
(gdb) target remote serial-device
其中,serialdevice指定串口设备,比如/dev/ttyS1。可以在命令后面添加baud选项设置串口连接的波特率,或者在使用target remote之前使用set remotebaud命令设置,关于gdb远程调试选项将会在下面具体介绍。当然也可以使用下面的命令建立TCP连接。
(gdb) target remote [tcp:][hostname]:portnumber
默认是使用TCP方式连接,因此可以不用显式地指定tcp。如果被调试程序和调试器是运行在同一平台上,还可以忽略主机名。注意,中间的冒号不能省略。若是要使用UDP方式,只需要将tcp换成udp就可以了。但相对于TCP,UDP是不可靠的。若使用UDP方式进行远程调试很可能会在繁忙或者不可靠的网络上丢弃包,从而影响调试过程。因此,推荐尽量使用TCP方式来建立调试连接。当远程调试过程完成后,可以通过detach命令将远程目标从宿主机gdb的控制下释放。远程目标被释放后会继续其正常的执行过程。在使用detach命令之后,宿主机gdb可以自由连接到另外一个目标。disconnect命令的行为类似detach,除了远程目标并不会继续恢复执行,它将等待gdb(这一实例或者另外一个)建立连接并继续调试。2. gdb远程调试选项gdb提供了许多选项专门用于远程调试,每种远程调试选项都可以通过set或show命令改变或显示当前选项值。
set remoteaddresssize bits
show remoteaddresssize
设置内存包中地址的最大值,单位为位(bit)。当传递地址到远程目标时,gdb将会屏蔽大于此位数的地址。默认值为目标地址的位数。可以通过show命令查看此选项值。
set remotebaud n
show remotebaud
设置远程串口I/O的波特率为n。这个值用来设置远程调试目标的串口传输速度。通过show命令查看远程连接的当前波特率。例如:
(gdb) show remotebaud
Baud rate for remote serial I/O is 4294967295.
(gdb) set remotebaud 115200
(gdb) show remotebaud
Baud rate for remote serial I/O is 115200
从输出结果可以看出,默认情况下,gdb并没有设置远程调试时的串口连接的波特率,其默认值为4294967295。用set命令将其设置为115200后,通过show命令可以验证当前波特率已经改变。
set remotebreak
show remotebreak
如果设置为On,当按下Ctrl C键来中断运行在远程目标的程序时,gdb将会发送Break信号给远程目标。如果设置为Off,gdb则发送Ctrl C字符。默认情况下,此选项设置为Off。
set remotelogbase base
show remotelogbase
设置记录远程串口协议通信的基数。默认值为ascii,另外还支持hex和octal。通过show命令可以查看当前远程串口协议通信记录的基数。
set remotelogfile file
show remotelogfile
设置记录远程通信信息的日志文件,默认不做记录。通过show命令显示当前日志文件名。
set remotetimeout num
show remotetimeout
设置等待远程目标响应的最大时限为num,默认为值2s。可以通过show命令显示当前等待远程目标响应的最大时限。
set remote hardware-watchpoint-limit limit
set remote hardware-breakpoint-limit limit
限制gdb远程调试的硬件断点或观察点的数量,默认值为4294967295。另外还有其他一些远程调试选项,读者可以自行查阅相关文档或者使用gdb help命令作进一步的了解。5.7内 核 调 试前面说过,对于应用程序来说,调试是软件开发过程中不可缺少的一个环节。对于Linux内核而言,调试同样重要。然而,Linux内核调试比起应用程序调试要困难得多。Linux内核的规模之庞大,往往让人望而却步,单靠阅读代码查找Bug已经非常困难。而Linux内核的开发人员出于保证内核代码正确性的考虑,不愿意在Linux内核源代码中添加调试器。他们认为在内核中加入调试器会误导开发者,从而引入不良的修改。所以对Linux内核进行调试一直是项艰苦的工作。调试工作的艰苦性正是内核级的开发有别于用户级程序开发的一个显著特点。尽管没有一些内置的调试内核的有效方法,但是随着Linux内核的不断完善,也逐渐形成了一些有效的监视内核代码和错误跟踪的技术。同时,许多第三方的针对Linux内核调试的补丁也应运而生,它们为标准的Linux内核提供了内核调试的功能。调试内核时,利用这些工具和方法可以有效地查找和判断Bug的位置和产生原因。5.7.1内核调试技术实际调试中,最普通的调试技术就是监视,即在应用程序编程中,在一些适当的地点调用printf函数显示监视信息。调试内核代码的时候,则可以用printk函数来完成相同的工作。通过使用打印函数,可以直接把关心的信息打印到终端或日志文件中,从而可以观察到程序执行过程中所关心的变量、指针等信息。Linux内核标准的系统打印函数是printk。printk函数具有良好的健壮性,不受内核运行条件的限制,在系统运行的任何阶段都可以使用。和C标准库中的printf函数不同的是,printk函数可以指定一个日志级别。内核根据这个级别来判断是否在终端上打印消息。内核把级别比某个特定值低的所有消息都显示在终端上。在头文件中定义了以下8种可用的日志级别。(1) KERN_EMERG: 用于紧急事件消息,它们一般是系统崩溃之前提示的消息。 (2) KERN_ALERT: 用于需要立即采取动作的情况。 (3) KERN_CRIT: 临界状态,通常涉及严重的硬件或软件操作失败。 (4) KERN_ERR: 用于报告错误状态,设备驱动多用此级来报告来自硬件的问题。 (5) KERN_WARNING: 警告可能出现问题,这类情况通常不会对系统造成严重问题。 (6) KERN_NOTICE: 正常情形的提示,许多与安全相关的状况用这个级别进行汇报。 (7) KERN_INFO: 提示信息,很多设备驱动启动时,用此级别打印相应的硬件信息。 (8) KERN_DEBUG: 用于调试信息。每个字符串(以宏的形式展开)表示成一个带尖括号的整数。整数值的范围为0~7,数值越小,优先级就越高。例如KERN_ALERT定义:
#defineKERN_ALERT”<1>”
printk默认采用的级别是DEFAULT_MESSAGE_LOGLEVEL,这个宏在文件kernel/printk.c中指定为一个整数值。在2.6内核里面的值是KERN_WARNING。
#define DEFAULT_MESSAGE_LOGLEVEL 4
但是在Linux的开发过程中,这个默认的级别值已经有过多次变化,因此建议读者在使用时始终指定一个明确的级别,例如:
printk(KERN_WARNING “This is a warning!\n”);
需要注意的是,在日志级别后不能忘记加上一个空格,否则会出错。通过printk函数打印信息需要重新编译内核,如果修改的是模块的话,那么只需要重新编译这个模块,而不需要编译整个内核,因为模块是可以动态加载的。内核消息是以环形队列的方式保存在一个大小为LOG_BUF_LEN的缓冲区中。该缓冲区的大小可以在编译内核的时候修改CONFIG_LOG_BUF_SHIFT选项进行修改,默认大小是16KB。也就是说,内核最多能保存大小为16KB的内核消息,如果内核消息的大小超出了缓冲区所能承受的最大值,旧的内核消息就会被新的消息所覆盖。使用这种机制的好处是,当某个问题产生大量的内核消息时也不会耗光内存。而使用环形的唯一缺点——可能会丢失旧的内核消息——带来的损失同简单性和健壮性相比,完全可以忽略不计。在标准的Linux系统上,用户空间的守护进程klogd首先从记录缓冲区中获取内核消息,然后通过另外一个守护进程syslogd保存到系统日志文件中。当系统加载内核及执行initrd时,会将内核信息记录在/proc/kmsg文件中。随后,klogd进程就可以从该文件中读取这些消息并处理,也可以通过syslog系统调用来读取这些消息。默认情况下,它选择从/proc中读取。处理的方式就是把消息传给syslogd守护进程,而后者会把接收到的所有消息保存到/var/log/messages文件中。根据日志级别内核可能会把消息打印到当前控制台上。如果优先级小于console_loglevel定义的整数值的话,消息才能显示出来。如果系统同时运行了klogd和syslogd两个守护进程,则无论console_loglevel定义为何值,内核消息都将被追加到/var/log/messages文件中(否则,除此之外的处理方式就依赖于对syslogd的设置)。如果klogd没有运行,这些消息就不会传递到用户空间,这种情况下,就只能查看/proc/kmsg了。内核出现错误往往很难处理,由于内核是整个系统的管理者,所以它不能采取用户空间的应用程序出现错误时所使用的简单处理手段,因为它很难自行修复,它也不能将自己杀死。遇到这种情况,内核通常会发布一个oops消息,随后内核会处于一种不稳定的状态,可能崩溃。通常oops消息中包含可供跟踪的回溯线索和CPU寄存器的内容。分析在内核发生崩溃时发送到终端的oops信息,这是Linux调试内核崩溃的传统方法。但是,原始的输出信息都是一些十六进制的内存地址,因此很难分析其内在意义。为了把这些数据解码成有意义的可供调试的信息,需要把它们解析为符号。旧版本的内核可以使用ksymoops工具来解码oops信息,它使用内核映像的System.map文件来解析产生错误的指令,并显示导致错误发生的回溯函数名称。但是在Linux 2.6内核引入了kallsyms特性之后,就无须使用ksymoops和System.map了。该特性可以通过定义CONFIG_KALLSYMS配置选项启用,该选项可以载入内核映像对应的内存地址的符号名称,所以内核可以直接显示回溯函数名称而不再打印难懂的十六进制数字。因为符号表被编译到内核映像中,所以内核会变大,但是对于开发人员来说,这样做是值得的。以上的几种调试技术可以称为错误跟踪技术,这些方法只能提供有限的调试能力,而不能提供源代码级的有效的内核调试手段。除了以上几种调试技术,还可以使用一种常用的内核调试工具kgdb。kgdb是一个在Linux内核上提供完整的gdb调试器功能的补丁。使用kgdb时需要两个系统——一个用于运行gdb,另一个用于运行待调试的内核。在2.6版本的Linux内核中,已经默认提供了对kgdb的支持,因此一般情况下,可以不用再打补丁,而只需要把kgdb配置进内核中并进行编译。5.7.2kgdb内核调试kgdb是一种插桩式(Stub)的内核调试机制,它提供Linux内核源代码级别的调试手段,通过配合使用远程gdb来调试Linux内核。使用kgdb可以像调试用户空间的应用程序那样,在内核中设置断点、单步跟踪运行内核和观察变量。使用kgdb需要两台机器——宿主机和目标板,两台机器之间通过串口或者以太网连接。目前,kgdb发布支持i386、x86_64、32bit PPC、SPARC等几种体系结构的调试器。要获得内核对kgdb调试机制的支持,需要为Linux应用kgdb补丁。补丁包括gdb stub、错误处理机制修改以及串口通信支持三个部分。其中,gdb stub是整个kgdb的核心部分,它处理来自远程机器上的gdb请求并控制目标板上的内核运行。通过修改错误处理机制,当一个不可预料的错误发生时,内核把控制权交给调试器。串口通信使用内核中的串口驱动,为内核中的gdb stub提供接口,它负责串口上的数据传送和接收。同样地,从gdb上发送的Ctrl Break请求也是由它来处理。kgdb其实是远程调试在Linux内核上的实现,它在内核中使用插桩的机制。内核在启动时等待远程调试器的连接,相当于实现了gdbserver的功能。然后,远程机器上的gdb负责读取内核符号表和源代码,并且尝试与之建立连接。一旦连接建立,就可以像调试普通程序那样调试内核了。kgdb的调试模型如图54所示。
图54kgdb调试模型
5.8网 络 调 试如果嵌入式平台之间需要进行网络通信,那么可能就需要使用嵌入式平台上的网络调试和诊断工具了。在嵌入式网络程序开发过程中,这些工具往往可以为开发人员提供很大的帮助。在Linux和众多类UNIX操作系统中,最为著名的网络调试和诊断工具非tcpdump莫属。在传统的网络分析和调试技术中,嗅探器(Sniffer)是最常见也是最重要的一种技术。嗅探器工具是专门为网络管理员和网络程序员进行网络分析而设计的。使用嗅探器可以随时掌握当前的网络状况,在网络性能下降或者出现故障时,可以通过嗅探器工具来分析原因,找出网络故障的来源。嗅探器工具实际上是网络上的一个抓包工具,同时可以对抓到的包进行分析,这在网络调试过程中非常有用。在共享式的网络中,数据包会以广播的形式发送给网络中所有主机,但是默认情况下,主机的网卡会自行判断该数据包是否该接收,这样就会抛弃不需要接收的数据包。而使用了嗅探器工具之后,它会拦截所有经过主机网卡的数据包,从而达到监听的效果。tcpdump就是一款功能强大、截取灵活的开源嗅探器工具,它广泛应用于很多类UNIX系统上。tcpdump,即dump traffic on a network,它可以根据使用者的定义有选择性地对网络上的数据包进行拦截,它支持针对网络层、协议、主机、网络或端口的过滤,并且提供and、or和not等逻辑关系运算符来加强过滤功能。tcpdump的精髓在于它的高效的过滤表达式。tcpdump通过过滤表达式指定要截取的数据包信息。如果不给出过滤表达式的话,则所有经过主机网卡的数据包都会被输出。如果明确给出了过滤表达式,则匹配此表达式的数据包信息才会被输出。tcpdump的输出信息既可以直接输出到终端上,也能够保存到指定文件以待分析。过滤表达式通常由一个或多个原语组成,每个原语前面可以有一个或多个修饰符。多个原语可以使用关系运算符构成关系原语,而多个表达式之间也可以用关系运算符组成更加复杂的表达式。表52列出了一些常用的修饰符,包括类型修饰符、方向修饰符和协议修饰符。
表52tcdump常用修饰符
修饰符类型含义
hostType表示主机netType表示网络portType表示端口srcDir指定网络传输的来源dstDir指定网络传输的目的地etherProto指定截取以太网数据包ipProto指定截取IP数据包arpProto指定截取ARP数据包rarpProto指定截取RARP数据包tcpProto指定截取TCP数据包udpproto指定截取UDP数据包
tcpdump支持的原语有很多,下面介绍一些常用的原语,一般使用下面的原语已经可以满足基本的网络调试需求,如表53所示。
表53tcpdump原语
原语含义
dst host hostname若数据包的目的主机为hostname,则为真src host hostname若数据包的来源主机为hostname,则为真host hostname若数据包的来源或目的主机为hostname,则为真dst net net若数据包的目的网络的网络号为net,则为真src net net若数据包的来源网络的网络号为net,则为真net net若数据包的来源或目的网络的网络号为net,则为真dst port portnum若数据包的目的端口为portnum,则为真src port portnum若数据包的来源端口为portnum,则为真port portnum若数据包的来源或目的端口为portnum,则为真less length等价于len≤length,若数据包的长度小于等于length,则为真greater length等价于len≥length,若数据包的长度大于等于length,则为真ip proto protocol若数据包为IP数据包且其协议为protocol,则为真ip broadcast若数据包为IP广播数据包,则为真ether multicast若数据包为以太网多播数据包,则为真ip multicast若数据包为IP多播数据包,则为真tcp, udp, icmp等价于ip proto protocol,若数据包是TCP、UDP或ICMP数据包,则为真
实际调试中,通过使用and、or和not等逻辑关系运算符,可以创建更加复杂的过滤表达式。tcpdump支持的关系运算符有下面这些。(1) !,not: 逻辑非。(2) &&,and: 逻辑与。(3) ||,or: 逻辑或。同时,tcpdump也支持下面这些比较运算符: >、=、<=、!=、=。若要访问数据包的特定位置的数据,可以使用以下的方法截取。
proto[expr:size]
其中,proto换成要访问的数据包所在的协议层,例如ether、ip、arp、rarp、tcp、udp、icmp等。中括号内的expr指定所要访问的数据的偏移量,而size是可选项,表示所要访问的数据的大小,单位为字节,允许的取值为1、2或4。tcpdump命令的语法如下。
tcpdump [ -AdDefIKlLnNOpqRStuUvxX ] [ -B buffer_size ] [ -c count ]
[ -C file_size ] [ -G rotate_seconds ] [ -F file ]
[ -i interface ] [ -m module ] [ -M secret ]
[ -r file ] [ -s snaplen ] [ -T type ] [ -w file ]
[ -W filecount ]
[ -E spi@ipaddr algo:secret,… ]
[ -y datalinktype ] [ -z postrotate-command ] [ -Z user ]
[ expression ]
可见,tcpdump有众多的命令行选项,由于篇幅有限,在这就不一一列出了,具体可以查看tcpdump的man文档。值得注意的是,过滤表达式应该位于所有命令选项之后。下面介绍一些常用的选项。(1) i interface: 指定监听的网络接口。(2) s snaplen: 设置捕获数据包的长度(3) v: 指定详细模式输出详细的数据包信息。(4) x: 指定以十六进制数格式显示数据包。(5) X: 指定以十六进制数格式显示数据包的同时,也输出相应的ASCII码形式。(6) S: 指定显示TCP的绝对顺序号,而不是相对顺序号。(7) e: 在输出行打印出数据链路层的头部信息。当然,除了tcmdump之外,还有一些常用的网络调试和诊断工具,例如arp、ping、route、netstat等,对于这些工具,想必读者或多或少用过,在这就不做具体介绍了。
小结本章详细讲解了嵌入式系统交叉开发模式的原理,分成宿主机环境、目标板环境和交叉编译环境三个部分来讲述。本章的另外一个重点是嵌入式系统开发过程中所使用的各种调试技术,包括gdb本地调试、远程调试、内核调试及网络调试,这些都是非常重要的调试手段,而且在嵌入式系统的开发过程中也是必不可少的一部分,因此希望读者真正掌握。进一步探索(1) 查阅相关文档,了解GNU工具的具体使用方法。(2) 了解并分析gdbstub的原理(http://sourceforge.net/projects/gdbstubs)。
评论
还没有评论。