···························································· 第5章chapter5 第5章Linux系统C语言程序设计1.1微型计算机简介C语言是Linux系统开发的主要语言,本章将讲述在Linux系统上用命令行方式编译、调试C语言程序的过程,之后介绍模块化程序设计和项目管理器的使用,最后介绍几种Linux系统的C语言集成开发环境。 5.1C语言概述〖*4/5〗5.1.1C语言发展过程C语言是由美国电话电报公司(AT&T)贝尔实验室正式发表的。它的原型是ALGOL 60语言。1963年,剑桥大学将ALGOL 60语言发展成为CPL(Combined Programming Language)。1967年,剑桥大学的Matin Richards 对CPL进行了简化,于是产生了BCPL。1970年,美国贝尔实验室的Ken Thompson将BCPL进行了修改,并为它起名B语言,并且用B语言编写了第一个UNIX操作系统。1973年,美国贝尔实验室的D.M.RITCHIE在B语言的基础上最终设计出了一种新的语言,这就是C语言。1978年,Brian W.Kernighian和Dennis M.Ritchie出版了名著The C Programming Language,从而使C语言成为目前世界上流行最广泛的高级程序设计语言。后来由美国国家标准学会在此基础上制定了一个C语言标准,于1983年发表,通常称之为ANSI C。 早期的C语言主要是用于UNIX系统。由于C语言的强大功能和各方面的优点逐渐为人们认识,到了20世纪80年代,C开始进入其他操作系统,并很快在各类大、中、小和微型计算机上得到了广泛的使用,成为当代最优秀的程序设计语言之一。许多早期著名的系统软件,如DBASE Ⅲ PLUS、DBASE Ⅳ、PC\|DOS、WORDSTAR等都是由C语言及汇编混合编写的。 C语言是一种结构化语言。它层次清晰,便于按模块化方式组织程序,易于调试和维护。C语言的表现能力和处理能力极强。它不仅具有丰富的运算符和数据类型,便于实现各类复杂的数据结构,还可以直接访问内存的物理地址,进行位(bit)一级的操作。由于C语言实现了对硬件的编程操作,因此C语言集高级语言和低级语言的功能于一体,既可用于系统软件的开发,也适合于应用软件的开发。此外,C语言还具有效率高、可移植性强等特点,因此被广泛地移植到了各类各型计算机上,从而形成了多种版本的C语言。目前最流行的C语言版本有Microsoft C(MS C)、Borland Turbo C(Turbo C)和AT&T C。这些C语言版本不仅实现了ANSI C标准,而且在此基础上各自做了一些扩充,使之更加方便、完美。 在C的基础上,1983年又由贝尔实验室的Bjarne Stroustrup推出了C++。C++进一步扩充和完善了C语言,成为一种面向对象的程序设计语言。C++的主要版本有Borland C++、Symantec C++和Microsoft Visual C++等。C++提出了一些更为深入的概念,它所支持的这些面向对象的概念容易将问题空间直接地映射到程序空间,为程序员提供了一种与传统结构程序设计不同的思维方式和编程方法。C是C++的基础,C++语言和C语言在很多方面是兼容的。 ◆嵌入式Linux程序设计第◆5章Linux系统C语言程序设计5.1.2C语言的特点〖*2〗1. 简洁紧凑、灵活方便C语言一共只有32个关键字,9种控制语句,程序书写自由,主要用小写字母表示。它把高级语言的基本结构和语句与低级语言的实用性结合起来。C语言可以像汇编语言一样对位、字节和地址进行操作,而这三者是计算机最基本的工作单元。 2. 运算符丰富 C的运算符包含的范围很广泛,共有34个运算符。C语言把括号、赋值、强制类型转换等都作为运算符处理。从而使C的运算类型极其丰富,表达式类型多样化,灵活使用各种运算符可以实现在其他高级语言中难以实现的运算。 3. 数据结构丰富 C的数据类型有: 整型、实型、字符型、数组类型、指针类型、结构体类型、共用体类型等。能用来实现各种复杂的数据类型的运算。并引入了指针概念,使程序效率更高。另外,C语言具有强大的图形功能,支持多种显示器和驱动器,且计算功能、逻辑判断功能强大。 4. C是结构式语言 结构式语言的显著特点是代码及数据的分隔化,即程序的各个部分除了必要的信息交流外彼此独立。这种结构化方式可使程序层次清晰,便于使用、维护以及调试。C语言程序是以函数形式提供给用户的,这些函数可方便地调用,并具有多种循环、条件语句控制程序流向,从而使程序完全结构化。 5. C语法限制不太严格、程序设计自由度大 一般的高级语言语法检查比较严,能够检查出几乎所有的语法错误。而C语言允许程序编写者有较大的自由度。 6. C语言允许直接访问物理地址,可以直接对硬件进行操作 C语言既具有高级语言的功能,又具有低级语言的许多功能,能够像汇编语言一样对位、字节和地址进行操作,而这三者是计算机最基本的工作单元,可以用来编写系统软件。 7. C语言程序生成代码质量高,程序执行效率高 C语言程序一般只比汇编程序生成的目标代码效率低10%~20%。 ◆嵌入式Linux程序设计第◆5章Linux系统C语言程序设计8. C语言适用范围大,可移植性好 C语言有一个突出的优点就是适合于多种操作系统,如DOS、UNIX,也适用于多种机型。 5.1.3C语言与Linux系统 C语言和Linux系统结合十分紧密,Linux系统本身是以C语言为主编写的。Linux系统内核提供的函数库、系统调用等都是以C语言原生方式提供,头文件、二进制库文件以及对应的C语言源代码可以很方便找到,为在Linux系统下开发C语言应用程序提供了很大的便利。此外,Linux系统提供了理想的C语言开发环境,包括: (1) 完善的编译环境,包括gcc、as、ld等编译、链接工具; (2) 强大的调试环境,主要是gdb工具; (3) 灵活的项目编译工具,如make工具; (4) 丰富的开源代码库等。 1. Linux系统中的C语言头文件 Linux系统中头文件放在/usr/include目录下,访问路径在环境变量中定义,说明如下。 (1) 常用头文件,存放在/usr/include根目录。 a.out头文件,定义了a.out执行文件格式和一些宏。 常数符号头文件,目前仅定义了i节点中i_mode字段的各标志位。 字符类型头文件,定义了一些有关字符类型判断和转换的宏。 错误号头文件,包含系统中各种出错号(Linus从minix中引进的)。 文件控制头文件,用于文件及其描述符的操作控制常数符号的定义。 信号头文件,定义信号符号常量、信号结构以及信号操作函数原型。 基本输入、输出函数头文件。 标准参数头文件,以宏的形式定义变量参数列表。 标准定义头文件,定义了NULL、offsetof(TYPE,MEMBER)。 字符串头文件,主要定义了一些有关字符串操作的嵌入函数。 终端输入输出函数头文件,主要定义控制异步通信口的终端接口。 时间类型头文件,主要定义了tm结构和一些有关时间的函数原型。 Linux标准头文件,定义了各种符号常数和类型,并声明了各种文件操作函数。 用户时间头文件,定义了访问和修改时间结构以及utime()原型。 (2) 体系结构相关头文件子目录include/asm。 这些头文件主要定义了一些与CPU体系结构密切相关的数据结构、宏函数和变量。 I/O头文件,以宏的嵌入汇编程序形式定义对I/O端口操作的函数。 内存复制头文件,含有memcpy()嵌入式汇编宏函数。 段操作头文件,定义了有关段寄存器操作的嵌入式汇编函数。 系统头文件,定义了设置或修改描述符/中断门等的嵌入式汇编宏。 (3) Linux内核专用头文件子目录include/linux。 内核配置头文件,定义键盘语言和硬盘类型(HD_TYPE)可选项。 软驱头文件,含有软盘控制器参数的一些定义。 文件系统头文件,定义文件表结构(file,buffer_head,m_inode等)。 硬盘参数头文件,定义访问硬盘寄存器端口、状态码和分区表等信息。 head头文件,定义了段描述符的简单结构和几个选择符常量。 内核头文件,含有一些内核常用函数的原型定义。 内存管理头文件,含有页面大小定义和一些页面释放函数原型。 调度程序头文件,定义了任务结构task_struct、初始任务0的数据,以及一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。 系统调用头文件,含有72个系统调用C函数处理程序,以“sys_”开头。 tty头文件,定义了有关tty_io、串行通信方面的参数、常数。 (4) 系统专用数据结构子目录include/sys。 文件状态头文件,含有文件或文件系统状态结构stat{}和常量。 定义了进程中运行时间结构tms以及times()函数原型。 类型头文件,定义了基本的系统数据类型。 系统名称结构头文件。 等待调用头文件,定义系统调用wait()和waitpid()及相关常数符号 2. 环境变量 (1) 头文件路径。Linux系统在编译时,会把/usr/include、/usr/local/include作为默认的头文件路径,如果开发人员在程序设计中需要包含的头文件不在默认的头文件路径,也不在当前项目所在路径,则可以通过设置gcc的环境变量 C_INCLUDE_PATH,CPLUS_INCLUDE_PATH或OBJC_INCLUDE_PATH来指定头文件目录。 (2) 库文件路径。一般 Linux 系统把 /lib 和 /usr/lib 两个目录作为默认的库搜索路径,所以使用这两个目录中的库时不需要进行搜索路径设置即可直接使用。对于处于默认库搜索路径之外的库,需要将库的位置添加到库的搜索路径之中。设置库文件的搜索路径有下列两种方式,可任选其一使用。 ① 在环境变量 LD_LIBRARY_PATH 中指明库的搜索路径。 例如: export LD_LIBRARY_PATH=/opt/gtk/lib:$LD_LIBRARY_PATH② 在/etc/ld.so.conf.d/libc.conf文件中添加库的搜索路径。将可能存放库文件的路径都加入到/etc/ld.so.conf.d/libc.conf中是比较合理的方法,这样就不用每次手动修改环境变量,将库文件的绝对路径直接写进去就可以了。 例如: /usr/X11R6/lib /usr/local/lib /opt/lib 5.2Linux系统C语言程序设计过程〖*4/5〗5.2.1Linux系统C语言程序流程在PC运行的Linux系统上,以命令方式开发C语言程序的流程如图51所示。 图51C语言开发流程图 首先用文本编辑器输入源代码,然后调用gcc命令将源代码编译成可执行的二进制文件,编译成功后,可以在文本命令方式下执行生成的文件。不同于Windows系统,可执行程序有特殊的扩展名,Linux系统中文件是否能执行,取决于当前用户是否对该文件具有执行权限,而与文件名及扩展名无关。 例1HelloWorld程序#include void main() { printf("Hello World!\\n"); return; } (1) 在用户的Home文件夹下新建文件夹app,新建hello.c文件,用gedit打开后,录入以上源代码。 (2) 在命令窗口,进入app文件夹,执行gcc hello.c o hello 命令,如果编译成功,则在app文件夹下可以看到生成的可执行文件hello,否则,根据错误提示,对源代码进行修改后重新编译,直到成功为止。 (3) 在文本命令窗口,执行./hello命令,可看到程序运行结果。 5.2.2gcc的编译选项 gcc是Linux C编译器的核心命令,有超过100个的可用选项,主要包括总体选项、告警和出错选项、优化选项和体系结构相关选项,具体如下。 1. 总体选项 gcc总体选项说明如表51所示。表51gcc总体选项说明选 项功 能 说 明c只是编译不链接,生成目标文件“.o”S只是编译不汇编,生成汇编代码E只进行预编译,不做其他处理g在可执行程序中包含标准调试信息o file把输出文件输出到file里v打印出编译器内部编译各过程的命令行信息和编译器的版本I 或include dir在头文件的搜索路径列表中添加dir目录L dir在库文件的搜索路径列表中添加dir目录static链接静态库l library链接名为library的库文件2. 告警选项 告警选项说明如表52所示。表52gcc告警选项说明选 项功 能 说 明ansi支持符合ANSI标准的C程序,关闭GNU C中某些不兼容ANSI C的特性pedantic允许发出ANSI C标准所列的全部警告信息w关闭所有告警W all允许发出gcc提供的所有有用的报警信息w error把所有的告警信息转化为错误信息,并在告警发生时终止编译过程3. 优化选项 gcc可以对代码进行优化,它通过编译选项“\|O n”来控制优化代码的生成,其中,n是一个代表优化级别的整数。对于不同版本的gcc来讲,n的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从0变化到2或3。 不同的优化级别对应不同的优化处理工作。 (1) “O 0”禁止编译器进行优化。默认为此项。 (2) “O 1”主要进行线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。使用优化选项“O 2”除了完成所有“O 1”级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。 (3) “O 3”还包括循环展开和其他一些与处理器特性相关的优化工作。 虽然优化选项可以加速代码的运行速度,但对于调试而言将是一个很大的挑战。因为代码在经过优化之后,原先在源程序中声明和使用的变量很可能不再使用,控制流也可能会突然跳转到意外的地方,循环语句也有可能因为循环展开而变得到处都有。所以建议在调试的时候不使用任何优化选项,只有当程序在最终发行的时候才考虑对其进行优化。 4. 体系结构选项 gcc体系结构选项说明如表53所示。表53gcc体系结构选项说明选项功 能 说 明march=typetype指定当前CPU的架构,针对不同的CPU使用相应的CPU指令。可选择的type有i386、i486、pentium及i686等mtune=type type指定CPU的型号mieeefp使用IEEE标准进行浮点数的比较mnoieeefp不使用IEEE标准进行浮点数的比较msoftfloat输出包含浮点库调用的目标代码mshort把int类型作为16位处理,相当于short intmrtd强行将函数参数个数固定的函数用ret NUM返回,节省调用函数的一条指令m32/64 生成32位/64位机器上的代码这些体系结构相关选项在嵌入式的设计中会有较多的应用,读者需根据不同体系结构将对应的选项进行组合处理。 5.3GDB调试器用法 调试是所有程序开发必备的手段。Windows系统程序开发工具大多提供集成开发环境,将编辑、调试及运行等功能集中在同一个界面,使用比较方便。Linux系统C语言程序由于是在命令方式下开发的,调试器也一样必须在命令方式下使用。虽然使用界面不够友好,但是调试功能也一样十分强大。 5.3.1gdb使用流程 调试的目的是解决程序设计中的逻辑错误。如果源代码编译通过,运行得到的结果与预期不一致,这时利用调试工具就可以比较快速地发现问题。进行调试时,gdb调试器输入的是编译后生成的可执行文件,如果编写程序中有语法错误,使用gcc进行语法检查时会报告错误,就不会生成可执行的文件,这种情况下不能使用gdb。 gdb调试器可以单步执行、跟踪、查看变量及内存的值等,这些功能依赖于生成的可执行程序中包含的调试信息。在可执行的二进制文件中包含调试信息,会增加程序文件的长度,同时降低程序的执行效率。使用gcc编译时,默认的情况下是不包含调试信息的。为了使用gdb调试器,必须在gcc命令中使用g选项以便在程序文件中包含调试信息。使用调试器修改程序错误后,应使用不带g选项的gcc命令重新编译生成可执行程序后,再进行发布。下面通过一个例子来讲解gdb的使用流程。 例2multi.c#include int multi(int m) { int s,i; s=0; for(i=1;i<=m;i++) s=s+1/i; return s; }void main() { int i; for(i=1;i<=20;i++) { printf("m=%d,s=%d\\n",i,multi(i)); } return; } 在gedit中录入上面的源代码保存退出后,首先使用gcc对multi.c进行编译,注意加上选项“\|g”,使得编译出的可执行代码中包含调试信息,否则gdb无法载入该可执行文件。#gcc -g mulit.c -o multi这段程序没有语法错误,利用gcc编译后,可成功生成可执行程序multi,但运行时发现,程序输出和预期结果不一致。 源程序由一个子函数multi(m)和主函数main()组成。 multi(m)的功能是计算1+12+…+1m的值,主函数通过循环调用multi(m),输出当m分别为1,2直到20时,函数multi(m)的计算结果。 但运行程序的输出,无论m的值是多少,multi(m)的结果全为1。 接下来,使用gdb调试器来分析查找程序中的错误。#gdb multi注意,gdb进行调试的是可执行文件multi,而不是multi.c源代码。 可以看到,出现一段gdb的版本号、使用的库文件等信息后,就进入了由“(gdb)”开头的命令行界面。 1. 查看文件 在gdb中输入“l”(list)就可以查看所载入文件对应的源码,如下所示。(gdb) l 1#include 2 3int multi(int m) 4{ 5int s,i; 6s=0; 7for(i=1;i<=m;i++) 8s=s+1/i; 9return s; 10} (gdb) l 11 12void main() 13{ 14int i; 15for(i=1;i<=20;i++) 16{ 17printf("m=%d,s=%d\\n",i,multi(i)); 18} 19return; 20} (gdb) l Line number 21 out of range; multi.c has 20 lines. (gdb)gdb列出的源代码中明确地给出了对应的行号,这样可以方便代码的定位。在gdb的命令中可使用缩略形式的命令,如“l”代表“list”,“b”代表“breakpoint”,“p”代表“print”等,读者也可使用“help”命令查看帮助信息。 2. 设置断点 设置断点是调试程序中一个非常重要的手段,它可以使程序到一定位置暂停它的运行。暂停后,程序员在该位置处可以方便地查看变量的值、内存情况等,从而找出代码的错误所在。 在gdb中设置断点非常简单,只需在“b”后加入对应的行号即可(这是最常用的方式,另外还有其他方式设置断点),如下所示。(gdb) b 8 Breakpoint 1 at 0x40053d: file multi.c,line 8.设置断点后,下次在调试器中运行程序时,程序就会在断点处暂停。如本例中,程序将暂停在第8行前。 3. 查看断点情况 在设置完断点之后,用户可以输入“info b”来查看设置断点情况,在gdb中可以设置多个断点。(gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x000000000040053d in multi at multi.c:84. 清除断点 当断点不需要时,可以使用“delete n”命令来清除断点,n为断点编号。delete可删除单个断点,也可删除一个断点的集合,这个集合用连续的断点号来描述。(gdb) d 1 (gdb) info b No breakpoints or watchpoints.除使用“delete”外,也可以使用“clear n”命令来清除断点。不同的是,clear后面的n为行号。