VIM+GCC+GDB编写,编译,运行,调试hello world
Linux操作系统也是学习和应用C语言的一个理想平台。目前,对于Linux平台下的C开发工程师有着大量的需求。所以,一个合格的程序员,必然需要学习Linux操作系统以及Linux上的C语言开发方法。
笔者在本书中使用ubuntu linux 10.0作为开发平台,向大家介绍如何在Linux平台上开发C语言程序。要在Linux上开发C语言,前提就是得安装一个Linux系统,这个既可以把Linux安装在机器上,也可以使用VMWare等虚拟机,在虚拟机中运行Linux。如何安装Linux不在本书的讲解范畴。大家可以通过相关书籍或者文章学习安装Linux操作系统。Linux操作系统有很多版本,比如ubuntu,比如federa,比如redhat。大家可以根据自己的喜好选择一个。本书以ubuntu为示例操作系统。
2.2.1 Linux命令
要在Linux系统下开发C语言程序,读者朋友们需要首先熟练掌握Linux的一些常用的终端命令,学会使用VIM工具,学会使用GCC编译工具,以及稍后大家接触到的GDB调试工具。
首先来学习一下Linux的一些常用命令。Linux操作系统虽然也提供了Windows类似的窗口管理,但是命令行控制依然是Linux操作系统的一个重要方面,很多工作可以很方便的通过命令行工具来完成。
首先,登录Linux操作系统之后,启动终端控制台。然后出现如下所示的终端控制台界面,闪烁的光标处是在并等待用户命令的输入。
pwd:显示当前目录
ls:查看当前的文件夹下面的内容
cd:切换目录
mkdir:创建文件夹
mv:移动文件或者文件夹
cp:拷贝文件或者文件夹
ps:显示进程
find:查找文件
grep:查找特定字符或者字符串
ln:创建符号链接
tar zvxf file.tar.gz:解压
tar zvcf file.tar.gz firdir:压缩
unzip xx.zip:解压
tail –f xx.log:显示文件末尾
wc –l file:显示文件的行数
rm:删除一个文件或者文件夹
more:查看文件内容
man:查看命令或者API文档
sudo apt-get install package ubuntu安装软件
sudo yum install package federo/redhat安装软件
批量杀进程
sudo kill -KILL `ps aux | grep nginx| awk '{print $2}'`
Linux下查看进程全路径:
ps -aux |grep xxxx获得PID
ls -l /proc/pid/里面的cwd对应的链接就是进程的路径
在Linux系统中,.表示当前目录,..表示父目录,~表示user目录,比如笔者的linux帐户是zyr,那么~就表示/home/zyr/目录。
由于本书并不是一本专门介绍Linux系统应用的书籍,所以只把Linux系统中一些常用的命令做了个简要的介绍,这些命令对于我们做C语言开发已经基本够用。如果对Linux系统的应用管理感兴趣的读者,可以专门阅读相关的书籍,进一步学习。
2.2.2 VIM使用
VIM是Linux上一个非常重要的文本编辑工具,C程序员可以通过VIM来编辑C源代码。Vim是从 vi 发展出来的一个文本编辑器。代码补完、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。和Emacs并列成为类Unix系统用户最喜欢的编辑器。
VIM有两种模式,一种是命令模式,一种是编辑模式。学习vim要首先过2关。第一关是理解vim的设计思路,vim设计之初就是整个文本编辑都用键盘而非鼠标来完成,键盘上几乎每个键都有固定的用法,且vim的制作者希望用户在普通模式(也就是命令模式,只可输入命令)完成大部分的编辑工作,将此模式设计为默认模式,初学者打开vim,如果直接输入单词,结果就会滴滴乱响,这是因为vim把用户输入的单词理解为命令了。第二关是命令关,vim有过百条命令对应编辑的需要,如果能熟练使用vim这些命令,编辑速度确实比鼠标要快很多,但是想全都记住它们也是一件难事。
首先,打开VIM之后,VIM默认的是命令模式。这个时候,你是无法输入字符的。而可以运行各种VIM命令。
如果读者朋友的Linux系统没有自带VIM程序,比如运行VIM命令,系统提示找不到该命令,那么需要首先安装VIM,才能使用。在Ubuntu系统中,运行:sudo
apt-get install vim-full来安装VIM工具。
vim filename: 如果filename对应的文件存在则打开,如果filename对应的文件不存在,则创建。
打开文件之后,可以操作下面的命令,来操作对应的文档:
j:向下移动光标
k:向上移动光标
l:向左移动光标
h:向右移动光标
ctrl+f:下翻页
ctrl+b:上翻页
w:write 保存
wq:保存并退出
q!:不保存强制退出
q:退出
ctrl+[:退出编辑状态
dd:删除一行
xp:交换2个字符
O:大写O,在光标之上插入一行,由命令模式进入编辑模式
O:小写O,在光标之下插入一行,由命令模式进入编辑模式
yy:复制一行
y:复制
x:剪切
i:光标当前位置插入,由命令模式进入编辑模式
a:光标之后的位置插入,由命令模式进入编辑模式
$:移动光标到行尾
^:移动光标到行首
gg:文件开头
G:文件末尾
%:跳到括号匹配处
ngg:跳到第N行
#:向后查找
*:向前查找
m,ns/a/b/g:在第M,N行之间,A替换为B
<<,>>:缩进
v->y:选择拷贝
ctrl +p:自动补齐。
上面的这些命令,读者朋友们可以在Linux系统中反复练习。掌握上面这些命令,开发C语言程序就基本上就够用了。VIM除了上面的命令之外,还有一个专门的配制文件,用来配置VIM的各种风格,包括语法高亮显示,自动缩进等功能,以让VIM的界面更加的友好。VIM配置文件的位置可以在:
l /etc/vim/vimrc
l ~/.vimrc
如果只对各个用户在自己的当前目录下的.vimrc修改的话,修改内容只对本用户有效,要想全部有效,可以修改/etc/vim/vimrc,则对所有用户有效。下面是一个比较典型的VIM配置文件。可以将下面的内容拷贝到vimrc中并保存。其中VIM中注释一条语句,是用”字符放在一行的开头,就把这一行注释了。
1.apt-get install vim-full
2.设置/etc/vim/vimrc加上下面语句:
"语法高亮度显示
syntax on
"去掉有关vi一致性模式,避免以前版本的一些bug和局限
set nocompatible
"显示行号
set number
"检测文件的类型
filetype on
"记录历史的行数
set history=1000
"背景使用黑色
set background=dark
"vim使用自动对齐,也就是把当前行的对起格式应用到下一行
set autoindent
"依据上面的对起格式,智能的选择对起方式,对于类似C语言编
set smartindent
"设置Windows风格的C/C++自动缩进,第一行设置tab键为4个空格,第二行设置当行之间交错时使用4个空格
set tabstop=4
set shiftwidth=4
"设置匹配模式,类似当输入一个左括号时会匹配相应的那个右括号
set showmatch
"去除vim的GUI版本中的toolbar
set guioptions-=T
"在编辑过程中,在右下角显示光标位置的状态行
set ruler
"默认情况下,寻找匹配是高亮度显示的,该设置关闭高亮显示
set nohls
"使用此设置会快速找到答案,当你找要匹配的单词时,别忘记回车
set incsearch
2.2.3 GCC编译程序
学会了VIM之后,就可以用VIM在Linux系统下用C语言写程序了。首先,打开Linux的终端控制台,然后执行cd命令,切换到当前用户目录下。比如笔者的当前目录就是:
/home/zyr/
在当前目录下,新建一个文件夹:projects。执行命令为:
mkdir projects
这样目录就建好了。执行:
cd projects
切换到projects目录下。然后执行:
vim main.c
打开了VIM编辑工具,创建了一个main.c文件,利用VIM在main.c文件中输入如下代码。然后执行:
Ctrl+[
退出编辑状态。
然后执行:
Shift+:
Wq
保存文件main.c之后退出。代码写好之后,剩下的任务就是如何将源文件编译为可执行程序。在Linux上要编译C代码,需要gcc或者g++工具。在控制台下运行gcc或者g++命令,如果系统提示命令未找到,则是因为系统没有安装这些编译工具。那么在ubuntu中执行下面的命令,可以为系统安装好编译所用到的各种工具:
sudo apt-get install build-essential
安装好了编译工具之后,再运行下面的命令:
gcc –o hello main.c
这条命令是什么意思呢?这就是用GCC工具把main.c源文件编译成可执行程序hello。GCC的各个参数如下:
GCC常用选项
选项
含义
--help
--target-help
显示 gcc 帮助说明。‘target-help’是显示目标机器特定的命令行选项。
--version
显示 gcc 版本号和版权信息
。
-o outfile
输出到指定的文件。
-x language
指明使用的编程语言。允许的语言包括:c
c++ assembler none 。
‘none’意味着恢复默认行为,即根据文件的扩展名猜测源文件的语言。
-v
打印较多信息,显示编译器调用的程序。
-###
与 -v 类似,但选项被引号括住,并且不执行命令。
-E
仅作预处理(code.i),不进行编译、汇编和链接。
-S
仅编译到汇编语言(code.s),不进行汇编和链接。
-c
编译、汇编到目标代码(code.o),不进行链接。
-pipe
使用管道代替临时文件。
-combine
将多个源文件一次性传递给汇编器。
其他GCC选项
更多有用的GCC选项:
命令
描述
-shared
生成共享目标文件。通常用在建立共享库时。
-static
禁止使用共享连接。
-l library
-llibrary
进行链接时搜索名为library的库。
例子: $
gcc test.c -lm -o test
-Idir
把dir 加入到搜索头文件的路径列表中。
例子: $
gcc test.c -I../inc_dir -o test
-Ldir
把dir 加入到搜索库文件的路径列表中。
例子: $
gcc -I/home/foo -L/home/foo -ltest test.c -o test
-Dname
预定义一个名为name 的宏,值为1。
例子: $ gcc -DTEST_CONFIG test.c -o test
-Dname =definition
预定义名为name ,值为definition 的宏。
-ggdb
-ggdblevel
为调试器 gdb 生成调试信息。level 可以为1,2,3,默认值为2。
-g
-glevel
生成操作系统本地格式的调试信息。-g 和 -ggdb 并不太相同, -g 会生成 gdb 之外的信息。level 取值同上。
-Wall
会打开一些很有用的警告选项,建议编译时加此选项。
-w
禁止显示所有警告信息。
Optimization
-O0
禁止编译器进行优化。默认为此项。
-O
-O1
尝试优化编译时间和可执行文件大小。
-O2
更多的优化,会尝试几乎全部的优化功能,但不会进行“空间换时间”的优化方法。
-O3
在 -O2 的基础上再打开一些优化选项:-finline-functions, -funswitch-loops 和 -fgcse-after-reload 。
-Os
对生成文件大小进行优化。它会打开 -O2 开的全部选项,除了会那些增加文件大小的。
Standard
-ansi
支持符合ANSI标准的C程序。这样就会关闭GNU
C中某些不兼容ANSI
C的特性。
-std=c89
-iso9899:1990
指明使用标准 ISO
C90 作为标准来编译程序。
-std=c99
-std=iso9899:1999
指明使用标准 ISO
C99 作为标准来编译程序。
命令执行完之后,如果一切顺利而程序没有各种语法错误,就会生成一个hello可执行程序。然后,运行编译好的程序如下:
./hello
程序就会在控制台程序上打印出一条hello
world字符串。如图所示。
一般来说,在Linux系统中,并不是通过命令行来编译源代码的,这样很不方便。于是有了一个叫Makefile的文件。这个文件可以来定义如何编译各种源文件,最后编译生成一个可执行文件。
同样以上面的hello world程序为例。将当前目录切换在Projects目录下,运行如下命令打开并编辑一个Makefile文件:
vim Makefile
然后在Makefile里输入如图所示的内容:
注意,2,4,6行都是以一个tab字符(按下键盘上的tab键)开始,否则编译的时候会有问题。执行:
wq
命令保存编辑好的Makefile文件。运行命令ls查看一下,你会发现在Projects目录下多了一个Makefile文件。然后运行:
make
gcc就会自动读取Makefile文件的内容,依次执行编译。最后链接成可执行的文件hello。这个时候ls一下,就会发现在projects目录下多了一个hello 和main.o。其中main.o是一个中间文件。编译完成之后,同样运行一下:
./hello
得到了同样的程序执行结果:打印输出了一个hello
world字符串。
上面的Makefile针对的是一个源文件。那么假如一个程序项目里有多个源文件,应该怎么写Makefile呢?
Makefile
1 .overpass:main.o http.o httppost.o
2. g++ -g -o overpass main.o http.o httppost.o -L. -lhiredis -lpthread
3. main.o:main.cpp
4. g++ -g -c main.cpp -o main.o
5. http.o:http.cpp
6.
g++ -g -c http.cpp -o http.o
7. httppost.o:httppost.cpp
8. g++
-g -c httppost.cpp -o httppost.o
9. clean:
10. rm
-f *.o overpass
其中,2,4,6,8,10行后面都是一个tab字符,这一点要特别注意,否则在执行make的时候,GCC会报错。
讨论了如何在Windows和Linux两大平台开发C语言程序之后,接着讨论两个与计算机程序设计关系紧密的两个问题:英语和数学对学习计算机程序设计的重要性。
2.2.4 GDB调试程序
GDB是Linux和UNIX环境下的一个强大而流行的程序调试器。一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序,首先在编译时,必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的 -g 参数可以做到这一点。如:
> cc -g hello.c -o hello
> g++ -g hello.cpp -o hello
如果没有-g,你将看不见程序的函数名、变量名,所代替的全是运行时的内存地址。当你用-g把调试信息加入之后,并成功编译目标代码以后,现在来看看如何用gdb来调试它。
启动GDB的方法有以下几种:
1、gdb <program>
program也就是你的执行文件,一般在当前目录下。
2、gdb <program> core
用gdb同时调试一个运行程序和core文件,core是程序非法执行后core dump后产生的文件,记录了程序在出错时的一些内存环境和出错信息。
3、gdb <program> <PID>
如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试它。program应该在PATH环境变量中搜索得到。
GDB启动时,可以加上一些GDB的启动开关,详细的开关可以用gdb -help查看。我在下面只例举一些比较常用的参数:
-symbols <file>
-s <file>
从指定文件中读取符号表。
-se file
从指定文件中读取符号表信息,并把它用在可执行文件中。
-core <file>
-c <file>
调试core dump的core文件。
-directory <directory>
-d <directory>
加入一个源文件的搜索路径。默认搜索路径是环境变量中PATH所定义的路径。
源程序:a.c
1 #include <stdio.h>
2
3 int func(int n)
4 {
5 int
sum=0,i;
6 for(i=0;
i<n; i++)
7 {
8 sum+=i;
9 }
10 return
sum;
11 }
12
13
14 void main(void)
15 {
16 int
i;
17 long
result = 0;
18 for(i=1;
i<=100; i++)
19 {
20 result
+= i;
21 }
22
23 printf("result[1-100]
= %d \n", result );
24 printf("result[1-250]
= %d \n", func(250) );
25 }
在Linux下编译生成执行文件:
gcc -g a.c -o a
下面使用GDB进行相关调试。
启动gdb:
gdb a
GNU gdb
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-suse-linux"...
(gdb) l <-------------------- l命令相当于list,从第一行开始例出原码。
1 #include <stdio.h>
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i<n; i++)
7 {
8 sum+=i;
9 }
10 return sum;
(gdb) <-------------------- 直接回车表示,重复上一次命令
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
(gdb) break a.c:16<-------------------- 设置断点,在源程序第16行处。
Breakpoint 1 at 0x8048496: file a.c, line 16.
(gdb) break func <-------------------- 设置断点,在函数func()入口处。
Breakpoint 2 at 0x8048456: file a.c, line 5.
(gdb) info break <-------------------- 查看断点信息。
Num Type Disp Enb Address What
1 breakpoint keep y 0x
2 breakpoint keep y 0x
(gdb) r <--------------------- 运行程序,run命令简写
Starting program: /home/xyz/ a
Breakpoint 1, main () at a.c:17 <---------- 在断点处停住。
17 long result = 0;
(gdb) n <--------------------- 单条语句执行,next命令简写。
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) n
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) c <--------------------- 继续运行程序,continue命令简写。
Continuing.
result[1-100] = 5050 <----------程序输出。
Breakpoint 2, func (n=250) at a.c:5
5 int sum=0,i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p i <--------------------- 打印变量i的值,print命令简写。
$1 = 134513808
(gdb) n
8 sum+=i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$2 = 1
(gdb) n
8 sum+=i;
(gdb) p i
$3 = 2
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$4 = 3
(gdb) bt <--------------------- 查看函数堆栈。
#0 func (n=250) at a.c:5
#1 0x080484e
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6
(gdb) finish <--------------------- 退出函数。
Run till exit from #0 func (n=250) at a.c:5
0x080484e
24 printf("result[1-250] = %d \n", func(250) );
Value returned is $6 = 31375
(gdb) c <--------------------- 继续运行。
Continuing.
result[1-250] = 31375 <----------程序输出。
Program exited with code 027. <--------程序退出,调试结束。
(gdb) q <--------------------- 退出gdb。
上面的例子向大家列举了GDB调试程序的基本用法。下面就利用GDB来调试一个segement fault错误:
1 : #include <stdio.h>
2 : #include <stdlib.h>
3 : int main(int argc, char **argv)
4 : {
5 : char
*buf;
6 :
7 : buf
= malloc(1<<32);
8 :
9 : fgets(buf,
1024, stdin);
10: printf("%s\n",
buf);
11:
12: return
1;
13: }
prompt> gcc -g segfault.c
prompt >./a.out
Hello World!
Segmentation fault
prompt >
prompt > gdb a.out
GNU gdb 5.0
Copyright 2000 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnu"...
(gdb)
首先运行这个程序看会发生什么情况:
(gdb) run
Starting program: /home/dgawd/cpsc/363/a.out
test string
Program received signal SIGSEGV, Segmentation fault.
0x4007fc
从上面运行的结果看出,在收到了SIGSEGV信号。该信号意味着程序在试着访问了一个无效的内存地址。先用backtrace命令看看:
(gdb) backtrace
#0 0x4007fc
#1 0x4007fb
#2 0x4007ef
#3 0x80484b
#4 0x
接下来只关心程序自己相关的代码,所以选择第3栈帧,看看程序是在什么地方崩溃的。
(gdb) frame 3
#3 0x80484b
10 fgets(buf, 1024, stdin)
程序很显然崩溃在第10行对fgets()的调用上。首先,可以认为fgets()本身工作正常(不然,那就会出现很多程序的错误)。那么问题一定出现在传递的参数上,而stdin是一个由stdio创建的全局变量,所以stdin没有问题。那么剩下唯一可以怀疑的地方就是变量buf了。所以查看buf的值:
(gdb) print buf
$1 = 0x0
Buf的值为NULL,这是错误的。不能传空指针给gets()。所以自然要检查程序第8行buf分配后的情况。于是先结束程序:
(gdb) kill
Kill the program being debugged? (y or n) y
然后在第8行设置断点:
(gdb) break segfault.c:8
Breakpoint 1 at 0x8048486: file segfault.c, line 8.
再运行程序:
(gdb) run
Starting program: /home/dgawd/cpsc/363/a.out
Breakpoint 1, main (argc=1, argv=0xbffffaf4) at segfault.c:8
8 buf = malloc(1<<32);
接着,来检查buf在分配前的值。由于buf没有被初始化,其中的值一定是垃圾数据:
(gdb) print buf
$2 = 0xbffffaa8 "��#\61*!*^_!fa*1"
然后,运行内存分配函数:
(gdb) next
10 fgets(buf, 1024, stdin);
(gdb) print buf
$3 = 0x0
调用malloc()后,buf的值为NULL。这意味着malloc()分配内存失败。再次检查内存分配语句:
7 : buf = malloc(1<<32);
发现1<<32的值为429497295,或者4GB。很少有机器的内存可以达到
prompt >
Hello World!
Hello World!
prompt >
gdb调试命令总结:
gdb hello
List
b-break for func,line
r-run
c-continue
s-step into
n-step over
p-print
q-quit
多线程:
gdb -p pid
set scheduler-locking on
info threads
thread 2
b functionname
c/s/n