概述:
由于目前国产桌面操作系统都是以linux内核为基础开发的,因此国产桌面操作系统应用程序开发实际上就是在linux系统上进行应用开发。当前软件种类繁多,应用场景可以说无所不在,软件种类虽多,但目前我们软件开发环境主要还是windows系统和linux系统环境两类,除了Qt这种跨平台软件在windows和linux系统上都有相同的开发环境外,linux系统软件开发和windows系统的开发环境还是有一定的区别。
Windows 系统下一般是使用集成开发环境(IDE),例如vxworks软件开发工具:workbench,arm软件开发工具:keil。这类工具之所叫集成开发环境是因为它们将代码编辑器、编译器、调试器这些软件开发工具集成在了一起。
Linux系统下的软件开发工具也包括编辑器、编译器、调试器这些工具,不同于集成开发环境,这些工具都是独立的。
由于C语言软件设计、编码规范等内容和开发环境关联不大,因此本文档没有包含这些内容,只涉及linux开发环境相关内容。本文档主要介绍linux环境C语音编译器、调试器的相关功能和使用方法,不包含编辑器内容,因为软件代码可以在windows环境编辑完成后在上传到linux系统,也可以使用linux系统上的gedit工具进行编辑,使用方式和windows相似,另外linux上的vi工具也可以。
gcc是GNU(开源软件组织)推出的免费开元软件之一,gcc不光能编译C语言,也能编译C++,objective-C 等语言,在本文中只讲述编译C语言。
gcc将C语言源文件编译成可执行程序分别要执行以下几个步骤:预处理、编译、汇编、链接四个步骤。下面通过一个简单的C程序演示各个过程。程序树下:
#include "stdio.h"
#define DISP_STR "hello world"
int main()
{
printf("display str:%s\n",DISP_STR);
return 0;
}
gcc对C源文件进行编译的第一步是通过预处理器(C preprocessor)对文件进行预处理,生成一个新文件,预处理的目的是展开源文件中的所有宏定义。
Linux终端运行命令gcc -E main.c > main.c.pre进行预处理,查看预处理后的文件如下:
…
int main()
{
printf("display str:%s\n","hello world");
return 0;
}
预处理后接着生成的预处理文件被gcc调用编译工具(compiler)进行编译,编译的结果生成汇编程序,编译通俗的讲法就是翻译,这里将C语言翻译成汇编语言。运行命令:gcc -S main.c
将上面的main.c文件编译成main.s如下所示(演示平台为x86):
.file "main.c"
.text
.section .rodata
.LC0:
.string "hello world"
.LC1:
.string "display str:%s\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.3.0-16ubuntu3) 7.3.0"
.section .note.GNU-stack,"",@progbits
编译之后,gcc使用汇编器(assembler)将汇编语言文件转换成目标文件(main.o),汇编之后的目标文件只包含符号信息。
运行命令:gcc -c main.c
生成目标文件main.o,main.o文件可以用工具objdump目标文件进行反汇编,例如终端输入命令:
objdump -d -s main.o,反汇编信息如下:
main.o: file format elf64-x86-64
Contents of section .text:
0000 554889e5 488d3500 00000048 8d3d0000 UH..H.5....H.=..
0010 0000b800 000000e8 00000000 b8000000 ................
0020 005dc3 .].
Contents of section .rodata:
0000 68656c6c 6f20776f 726c6400 64697370 hello world.disp
0010 6c617920 7374723a 25730a00 lay str:%s..
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7.
0010 332e302d 31367562 756e7475 33292037 3.0-16ubuntu3) 7
0020 2e332e30 00 .3.0.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 23000000 00410e10 8602430d ....#....A....C.
0030 065e0c07 08000000 .^......
Disassembly of section .text:
0000000000000000
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # b
b: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 12
12: b8 00 00 00 00 mov $0x0,%eax
17: e8 00 00 00 00 callq 1c
1c: b8 00 00 00 00 mov $0x0,%eax
21: 5d pop %rbp
22: c3 retq
汇编之后,gcc使用连接器将目标文件main.o和C语言的标准库链接在一起,生成一个可运行的执行程序。Linux系统下生成的可以执行文件并不需要像windows系统那样已.exe结尾,可以是任意名称。
输入命令:gcc -o test_gcc main.c
生成名称为“test_gcc”的执行程序,终端输入命令./test_gcc运行该程序,结果如下:
[jingjia@ubuntu1804/home/developer/samba/zhousonglin/gcc_test]$./test_gcc
display str:hello world
以上例子链接了C库,为了更好的说明gcc的链接功能,下面链接一个我们自定义的库文件,做如下修改:
1、在以上程序基础上增加一个文件:foo.c文件内容如下:
#include "stdio.h"
void foo()
{
printf("this is foo\n");
}
输入命令:ar crs libfoo.a foo.o,生成libfoo.a库文件
也可以生成动态库:
gcc -fPIC -shared -o libfoo.so foo.o
输入命令:cp libfoo.so /usr/local/lib将动态库拷贝到系统默认链接目录下,这样运行时才能找到对应库文件。运行命令sudo ldconfig始更改生效。
2、修改main.c增加对foo函数的调用:
#include "stdio.h"
#define DISP_STR "hello world"
extern void foo();
int main()
{
printf("display str:%s\n",DISP_STR);
foo();
return 0;
}
输入命令:gcc -o test_gcc main.c -L./ -lfoo,生成执行程序“test_gcc”,运行结果如下:
display str:hello world
this is foo
本章小结:本章通过一个简单的程序介绍了gcc工具编译C程序的整个过程,以上只介绍了gcc很小一部分功能,整个gcc工具非常强大,读者如果像进一步了解可以阅读《Using the GNU Compiler Collection》手册。
上一章中我们使用gcc编译程序都是通过手动输入命令的方式,虽然例子中的程序文件很简单,但每次编译都要重新输入命令确实很麻烦,而且我们的真实项目一般具有多个文件,而且还要链接各种库文件,在这种情况下就需要对项目文件进行编译管理,linux系统下make和Makefile能方便的对项目工程进行编译管理,make工具通过解析Makefile文件对项目程序进行编译管理。
Makefile的规则由以下三部分组成:
目标:要生成的文件
依赖:生成目标依赖的其他目标
命令:告诉make如何生成目标(注意:命令必须TAB键开始)
规则格式如下:
target:prerequisites
command
…
Make解析Makefile文件时如果没有指定目标,默认从第一个目标开始执行,否则从指定目标开始执行。为了通过make生成最终目标,需要产生大量中间目标,makefile规则所描述的依赖关系将所有的目标关联在一起,最终生成库和创建可执行文件。
下面通过一个简单的Makefile,演示不同目标生成过程(命令前加‘@’符号作用不显示命令)Makefile文件的名称一般用“Makefile”(文件名称也可以是其他名字,但运行make时需要加-f参数,例如:make -f makefiletest):
终端输入make命令:
all:test
@echo "hello world"
test:
@echo "do test"
运行结果:
do test
hello world
由结果可知,默认先生成目标”all”,由于目标”all”依赖目标“test”,所以先生成目标“test”,然后生成目标“all”。
也可以指定生成某个目标,例如输入命令:make test:
运行结果:
do test
用上章的例子代码演示简单Makefile的编写:
main.c代码:
#include "stdio.h"
#define DISP_STR "hello world"
int main()
{
printf("display str:%s\n",DISP_STR);
return 0;
}
Makefile内容:
test.exe:main.o
gcc -o test.exe main.o
main.o:main.c
gcc -c -o main.o main.c
终端运行make命令,当前目录下生成目标文件
通过熟悉上面简单的Makefile文件我们已经了解Makefile的结构和运行机制。以上makefile形式虽然也可以编译、产生执行程序,但是可维护性不好,当我们项目中增加,减少文件时都需要修改Makefile文件,例如需要增加一个前面例子的foo.c源文件,main.c文件增加对foo.c中函数的引用:
foo.c文件内容:
#include "stdio.h"
void foo()
{
printf("this is foo\n");
}
main.c文件内容:
#include "stdio.h"
#define DISP_STR "hello world"
extern void foo();
int main()
{
printf("display str:%s\n",DISP_STR);
foo();
return 0;
}
Makefile文件:
test.exe:main.o foo.o
gcc -o test.exe main.o foo.o
main.o:main.c
gcc -c -o main.o main.c
foo.o:foo.c
gcc -c -o foo.o foo.c
从上面例子可以看到增加一个源文件会导致Makefile需要修改多处。实际项目中源文件多,而且改动频繁,这样结构的Makefile可维护性太差。下面通过介绍Makefile的其他一些语法特性增加Makefile的可维护性。
项目中通常需要对某些已s生成的目标文件进行清理,这种情况下就需要用到假目标,假目标没有依赖文件,只有命令,运行make时只要指定对应目标就会执行目标命令,假目标采用”.PHONY”来定义(字母大写)。在以上Makefile例子中增加clean假目标来清除生成的文件,Makefile文件内容如下:
.PHONY: clean
test.exe:main.o foo.o
gcc -o test.exe main.o foo.o
main.o:main.c
gcc -c -o main.o main.c
foo.o:foo.c
gcc -c -o foo.o foo.c
clean:
rm foo.o main.o test.exe
Makefile中的变量可提供Makefile 的可维护性,引用变量采用$(变量名)或${变量名}的形式,以上例子中增加以下变量:
.PHONY: clean
CC = gcc
RM = rm
EXE = test.exe
OBJS = main.o foo.o
$(EXE):$(OBJS)
$(CC) -o $(EXE) $(OBJS)
main.o:main.c
$(CC) -c -o main.o main.c
foo.o:foo.c
$(CC) -c -o foo.o foo.c
clean:
$(RM) -rf $(OBJS) $(EXE)
引入变量的好处是当需要修改变量相关内容时不需要进行多处修改,只需要修改变量定义处。
上面增加自定义变量后虽然一定程度上增加了Makefile的维护性,但是依赖文件还是需要多次修改,自动变量可以进一步增加Makefile的维护性,自动变量是make工具默认的,不需要我们重新定义,Makefile有以下常用的自动变量:
$@:表示一个规则中的目标,当规则中有多个目标“@”表示任何导致规则中命令被执行的目标。
$^:表示规则中所有依赖条件
$<:表示规则中第一个依赖条件
用自动变量替换后的makefile文件如下:
.PHONY: clean
CC = gcc
RM = rm
EXE = test.exe
OBJS = main.o foo.o
$(EXE):$(OBJS)
$(CC) -o $@ $^
main.o:main.c
$(CC) -c -o $@ $^
foo.o:foo.c
$(CC) -c -o $@ $^
clean:
$(RM) -rf $(OBJS) $(EXE)
修改后的Makefile虽然看上去简单了一些,但每个源文件依然都要单独写一个规则,我们可以通过模式进一步简化。
模式类似于正则匹配符功能,上面的例子使用模式后可以修改成如下形式:
.PHONY: clean
CC = gcc
RM = rm
EXE = test.exe
OBJS = main.o foo.o
$(EXE):$(OBJS)
$(CC) -o $@ $^
%.o:%.c
$(CC) -c -o $@ $^
clean:
$(RM) -rf $(OBJS) $(EXE)
运用模式的Makfile进一步简化了,增减源文件时只需要修改“OBJS”变量增删对应的目标文件名。下面通过Makefile的函数进一步简化。
函数是makefile的重要功能,通过使用函数能极大的简化Makefile编写,下面介绍两个常用的函数作为示例来了解函数的用法。
$(wildcard pattern):通配符函数,通过该函数可以得到当前目录下所有满足pattern模式的文件名或目录名列表。
$(patsubst pattern, replacement, text):模式替换函数,该函数将text列表中满足pattern模式的名称替换成replacement模式名称。
以上Makefile用上面两个函数替换后如下:
.PHONY: clean
CC = gcc
RM = rm
EXE = test.exe
SRC = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
$(EXE):$(OBJS)
$(CC) -o $@ $^
%.o:%.c
$(CC) -c -o $@ $^
clean:
$(RM) -rf $(OBJS) $(EXE)
引入函数后进一步简化了Makefile,目前增加、删除源文件不需要对Makefile进行修改,可以直接运行make编译。例如增加一个bar.c文件,代码如下:
#include "stdio.h"
void bar()
{
printf("this is bar\n");
}
修改main.c增加对bar()函数调用:
#include "stdio.h"
#define DISP_STR "hello world"
extern void foo();
extern void bar();
int main()
{
printf("display str:%s\n",DISP_STR);
foo();
bar();
return 0;
}
终端输入:make clean、make进行编译,生成test.exe执行文件,输入命令./test.exe运行结果如下:
display str:hello world
this is foo
this is bar
整个过程Makefile无需任何修改。
2.3.6.1 静态库编译
将以上例子中的bar.c和foo.c文件编译成静态库文件,步骤如下:
#ifndef __TEST__H_
#define __TEST__H_
extern void foo();
extern void bar();
#endif
3、创建makefile文件如下所示:
.PHONY: clean
CC = gcc
RM = rm
LIB = libtest.a
SRC = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
ARFLAGS = crs
AR = ar
$(LIB):$(OBJS)
$(AR) $(ARFLAGS) $@ $^
%.o:%.c
$(CC) -c -o $@ $^
clean:
$(RM) -rf $(OBJS) $(LIB)
输入make命令后在lib目录下生成libtest.a库文件。
2.3.6.2 动态库编译
如果要生成动态库文件,makefile修改如下:
.PHONY: clean
CC = gcc
RM = rm
SLIB = libtest.a
DLIB = libtest.so
SRC = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
ARFLAGS = crs
AR = ar
CFLAGS = -fPIC -shared
all:$(SLIB) $(DLIB)
$(SLIB):$(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DLIB):$(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o:%.c
$(CC) -c -o $@ $^
clean:
$(RM) -rf $(OBJS) $(SLIB) $(DLIB)
输入make命令后在lib目录下同时生成libtest.a和libtest.so库文件。
2.3.6.3 库文件链接
1、main.c调用库文件接口,代码如下:
#include "stdio.h"
#include "libtest.h"
#define DISP_STR "hello world"
int main()
{
printf("display str:%s\n",DISP_STR);
foo();
bar();
return 0;
}
2、修改Makefile链接上面生成的库,生成执行文件,修改后如下所示:
.PHONY: clean
CC = gcc
RM = rm
EXE = test.exe
SRC = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
LDLIBS = -ltest
LDPATH = ./lib
INCPATH = ./lib
$(EXE):$(OBJS)
$(CC) -o $@ $^ -L$(LDPATH) $(LDLIBS)
%.o:%.c
$(CC) -I$(INCPATH) -c -o $@ $^
clean:
$(RM) -rf $(OBJS) $(EXE)
3.输入make命令在当前目录下生成test.exe执行文件。
本章小结:本章通过一个简单的程序例子介绍了makefile的基本组成和编写方式,以上Makefile例子可以满足一般的程序编译,但离规范化还有一定差异,如果想进一步了解Makefile编写,可以通过阅读《GNU Make》手册深入学习。
通过集成开发环境(IDE)调试软件非常方便,步骤通常是开始调试->打断点->运行程序到断点位置->单步运行->查看对应寄存器或变量值->停止调试。在windows环境以上步骤通常都可以通过图形界面进行操作,在linux环境通过gdb调试器也可以对程序进行调试。
使用gdb调试程序前需要程序编译时带上调试信息,修改上一章例子中断Makefile:编译目标文件时通过增加“-g”参数在生成的目标文件中添加调试信息,修改后s如下:
.PHONY: clean
CC = gcc
CFLAGS = -g -c
RM = rm
EXE = test.exe
SRC = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
$(EXE):$(OBJS)
$(CC) -o $@ $^
%.o:%.c
$(CC) $(CFLAGS) -o $@ $^
clean:
$(RM) -rf $(OBJS) $(EXE)
运行make命令生成test.exe执行程序。终端输入命令:gdb +程序名的方式启动gdb调试器,例如运行命令gdb ./test.exe:
gdb提供了两种方式在线运行程序:
gdb设置断点有以下2种常用方式设置断点:
输入:info b(break)命令可以查看当前调试程序所有设置断点位置,例如在函数foo和bar调用处打上断点后查看断点信息如下:
输入 :d(delete) + 断点号命令可以删除对应断点,以上例子中输入d 1后,查看断点信息如下所示:
在需调试位置打上断点、运行程序,调试程序运行到断点处后通常需要进行单步调试来进一步定位具体问题。gdb单步调试相关命令有以下几个:
#include "stdio.h"
#define DISP_STR "hello world"
extern void foo();
extern void bar();
static int i = 0;
int main()
{
printf("display str:%s\n",DISP_STR);
i++;
foo();
i++;
bar();
return 0;
}
输入l(list) 可以查看断点附近源代码,依次输入命令可以连续查看断点之后的代码,例如:
输入q(quit)可以退出调试,Quit anyway? (y or n) 处输入’y’,s例如:
本章小结:本章介绍了gdb调试器的基本功能和用法,通过本章内容我们基本可以对开发的程序进行在线调试,对于一些复杂工程我们可能还有用到gdb的其他功能,如果想深入了解gdb可以阅读《Debugging with gdb》手册。
总结:本文档分三章内容主要介绍了linux系统环境下C语言程序开发所使用的常用工具:编译器、编译管理工具、调试器。为了方便新手快速上手,文档对于各工具只是进行了初步讲解,希望对linux应用开发入门有所帮助,想深入熟悉和理解还需要在项目中不断练习。