一、实验目的
1.掌握C语言编译的基本方法。
2.掌握gdb调试工具的基本用法。
二、实验注意事项
实验室内的实验环境与系统是共用设施,请不要在系统内做对系统或对其他用户不安全的事情。
要求每个同学登录后系统后,要在自己的家目录下创建一个属于自己的子目录(以自己(拼音)名字或学号)。以后所有工作都要在自己的目录内进行。建议以后的实验都在同台计算机上做,这样可以保持连续性。
用户要按通常实验要认真书写实验报告。
三、实验内容及步骤
1. 用gcc带不同参数编译下列hello.c程序。
#include <stdio.h>
int main()
{
printf(”Hello World!\n”);
return 0;
}
(1)只作预处理,生成hello.i,相应命令为: gcc -E hello.c -o hello.i
(2)只进行编译,不做汇编,生成汇编代码,命令为: gcc -S hello.c
(3)只进行汇编,不做连接,生成目标代码,命令为: gcc -c hello.c
(4)以默认方式生成可执行程序a.out,命令为: gcc hello.c
(5)生成可执行程序hello,命令为: gcc hello.c -o hello
2.Linux下gcc的生成和使用动态库和静态库
程序1: hello.h
#ifndef HELLO_H #define HELLO_H void hello(const char *name); #endif |
程序2:hello.c
#include <stdio.h> void hello(const char *name) { printf("Hello %s!\n", name); } |
程序3:main.c
#include "hello.h" int main() { hello("everyone"); return 0; } |
注意:这个时候,我们编译好的hello.o是无法通过gcc –o 编译的,这个道理非常简单,hello.c是一个没有main函数的.c程序,因此不够成一个完整的程序,如果使用gcc –o 编译并连接它,GCC将报错。
无论静态库,还是动态库,都是由.o文件创建的。因此,我们必须将源程序hello.c通过gcc先编译成.o文件。
这个时候我们有三种思路:
1) 通过编译多个源文件,直接将目标代码合成一个.o文件。
2) 通过创建静态链接库libmyhello.a,使得main函数调用hello函数时可调用静态链接库。
3) 通过创建动态链接库libmyhello.so,使得main函数调用hello函数时可调用动态链接库。
思路一:编译多个源文件
命令为:
思路二:静态链接库
静态库文件名的命名规范是以lib为前缀,紧接着跟静态库名,扩展名为.a。例如:我们将创建的静态库名为myhello,则静态库文件名就是libmyhello.a。在创建和使用静态库时,需要注意这点。创建静态库用ar命令。
在系统提示符下键入以下命令将创建静态库文件libmyhello.a。
# ar rcs libmyhello.a hello.o
在主程序main中直接调用公用函数hello,生成目标程序hello的命令为:
删除静态库文件试试公用函数hello,看是否真的连接到目标文件 hello,命令为:
思路三、动态链接库(共享函数库)
静态链接库的一个缺点是,如果我们同时运行了许多程序,并且它们使用了同一个库函数,这样,在内存中会大量拷贝同一库函数。这样,就会浪费很多珍贵的内存和存储空间。使用了共享链接库的Linux就可以避免这个问题。
共享函数库和静态函数在同一个地方,只是后缀有所不同。比如,在一个典型的Linux系统,标准的共享数序函数库是/usr/lib/libm.so。
当一个程序使用共享函数库时,在连接阶段并不把函数代码连接进来,而只是链接函数的一个引用。最终的函数导入内存开始真正执行时,函数引用被解析,共享函数库的代码才真正导入到内存中。这样,共享链接库的函数就可以被许多程序同时共享,并且只需存储一次就可以了。共享函数库的另一个优点是,它可以独立更新,与调用它的函数毫不影响。
在系统提示符下键入以下命令得到动态库文件libmyhello.so
# gcc -shared -fPIC -o libmyhello.so hello.o
“PIC”命令行标记告诉GCC产生的代码不要包含对函数和变量具体内存位置的引用,这是因为现在还无法知道使用该消息代码的应用程序会将它连接到哪一段内存地址空间。这样编译出的hello.o可以被用于建立共享链接库。建立共享链接库只需要用GCC的”-shared”标记即可。
运行gcc命令生成目标文件,命令为:
错误提示,找不到动态库文件libmyhello.so。程序在运行时,会在/usr/lib和/lib等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。有多种方法可以解决,
(1)我们将文件 libmyhello.so复制到目录/usr/lib中,再试试。
# mv libmyhello.so /usr/lib
# ./hello
成功!
(2)既然连接器会搜寻LD_LIBRARY_PATH所指定的目录,那么我们可以将这个环境变量设置成当前目录:
先执行:
export LD_LIBRARY_PATH=`pwd`
再执行:
./hello
成功!
LD_LIBRARY_PATH:这个环境变量指示动态连接器可以装载动态库的路径。
3. 使用gdb调试下列程序,练习gdb命令。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void my_print (char *string)
{
printf ("The string is %s\n", string);
}
void my_print2 (char *string)
{
char *string2;
int size, i;
size = strlen (string);
string2 = (char *) malloc (size + 1);
for (i = 0; i < size; i++)
string2[size - i] = string[i];
string2[size+1] = `\0';
printf ("The string printed backward is %s\n", string2);
}
int main ()
{
char my_string[] = "hello there";
my_print (my_string);
my_print2 (my_string);
}
用下面的命令编译它:
gcc –g greeting.c -o greeting
这个程序执行时显示如下结果:
The string is hello there
The string printed backward is
输出的第一行是正确的, 但第二行打印出的东西并不是我们所期望的。我们所设想的输出应该是:
The string printed backward is ereht olleh
由于某些原因, my_print2 函数没有正常工作。让我们用 gdb 看看问题究竟出在哪儿, 先键入如下命令:
gdb greeting
注意: 记得在编译 greeting 程序时把调试选项打开。
如果你在输入命令时忘了把要调试的程序作为参数传给 gdb , 你可以在 gdb 提示符下用 file 命令来载入它:
(gdb) file greeting
这个命令将载入 greeting 可执行文件就象你在 gdb 命令行里装入它一样。
这时你能用 gdb 的 run 命令来运行 greeting 了。 当它在 gdb 里被运行后结果大约会象这样:
(gdb) run
Starting program: /home/student/greeting
The string is hello there
The string printed backward is
Program exited normally.
这个输出和在 gdb 外面运行的结果一样。 问题是, 为什么反序打印没有工作? 为了找出症结所在, 我们可以在 my_print2 函数的 for 语句后设一个断点, 具体的做法是在 gdb 提示符下键入 list 命令三次, 列出源代码.
技巧: 在 gdb 提示符下按回车健将重复上一个命令.
第一次键入 list 命令的输出如下:
1 #include<stdio.h>
2 #include<string.h>
3 #include<stdlib.h>
4 void my_print (char *string)
5 {
6 printf ("The string is %s\n", string);
7 }
8 void my_print2 (char *string)
9 {
- *string2;
如果按下回车,gdb将再次执行一次list命令,给出下列输出;
11 int size, i;
12 size = strlen (string);
13 string2 = (char *) malloc (size + 1);
14 for (i = 0; i < size; i++)
15 string2[size - i] = string[i];
16 string2[size+1] = ‘\0';
17 printf ("The string printed backward is %s\n", string2);
18 }
19 int main ()
20 {
再按一次回车将列出 greeting 程序的剩余部分:
21 char my_string[] = "hello there";
22 my_print (my_string);
23 my_print2 (my_string);
24 return 0;
25 }
根据列出的源程序, 你能看到要设断点的地方在第15行, 在 gdb 命令行提示符下键入如下命令设置断点:
(gdb) break 15
gdb 将作出如下的响应:
Breakpoint 1 at 0x139: file greeting.c, line 15
(gdb)
现在再键入 run 命令, 将产生如下的输出:
Starting program: /home/student/greeting
The string is hello there
Breakpoint 1, my_print2 (string = 0xbfffdc4 "hello there") at greeting.c :15
15 string2[size-i]=string[i]
这时可通过设置一个观察点,观察 string2[size - i] 变量的值来看错误是怎样产生的, 做法是键入:
(gdb) watch string2[size - i]
gdb 将作出如下回应:
Watchpoint 2: string2[size - i]
现在可以用 next 命令来一步步的执行 for 循环了:
(gdb) next
经过第一次循环后, gdb 告诉我们 string2[size - i] 的值是 `h`. gdb 用如下的显示来告诉你这个信息:
Watchpoint 2, string2[size - i]
Old value = 0 `\000'
New value = 104 `h'
my_print2(string = 0xbfffdc4 "hello there") at greeting.c:14
14 for (i=0; i<size; i++)
这个值正是期望的. 后来的数次循环的结果都是正确的. 当 i=10 时, 表达式 string2[size - i] 的值等于 `e`, size - i 的值等于 1, 最后一个字符已经拷到新串里了.
如果你再把循环执行下去, 你会看到已经没有值分配给 string2[0] 了, 而它是新串的第一个字符, 因为 malloc 函数在分配内存时把它们初始化为空(null)字符. 所以 string2 的第一个字符是空字符. 这解释了为什么在打印 string2 时没有任何输出了.
现在找出了问题出在哪里, 修正这个错误是很容易的,你得把代码里写入 string2 的第一个字符的的偏移量改为 size - 1 而不是 size. 这是因为 string2 的大小为 12, 但起始偏移量是 0, 串内的字符从偏移量 0 到 偏移量 10, 偏移量 11 为空字符保留。
修改后程序:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void my_print(char *string)
{
printf("The string is %s\n",string);
}
void my_print2(char *string)
{
char *string2;
int size,i;
size=strlen(string);
string2=(char*)malloc(size + 1);
for(i=0;i<size;i++)
string2[size-1-i]=string[i];
string2[size]='\0';
printf("The string printed backward is %s\n",string2);
}
int main()
{
char my_string[]="hello there";
my_print(my_string);
my_print2(my_string);
return 0;
}
4.利用gcc 编译下列C 语言程序,查看运行结果,如果有错用gdb调试之。
//程序名为bugging.c
#include<stdio.h>
double average_(int array[],int *min,int *max,int num)
{
int i ,s=0;
double ave;
*min=*max=array[0];
for(i=1;i<num;i++)
{
if(array[i]>*max)*max=array[i];
if(array[i]<*min)*min=array[i];
s+=array[i];
}
ave=s*1.0/num;
return ave;
}
int main(void)
{
int max,min;
int a[5]={3,5,1,4,6};
double aver;
aver=average_(a,&min,&max,5);
printf("min=%d,max=%d,aver=%d\n",min,max,aver);
return 0;
}
编译该程序:
$ gcc -o bugging -g bugging.c
运行该程序:
$ ./bugging
机子作出如下响应:
min=1,max=5,aver=2.000000
运算结果不对,为了查找该程序中出现的问题,我们利用gdb,并按如下的步骤进行:
(1)进入 gdb,装入可执行文件,命令为:
(2)利用list 命令查看程序代码,命令为;
(3)继续查看,命令为:
(4)平均值结果不对,所以肯定是子函数有问题,因此可以在子函数入口处设一断点,命令为:
(5)要得到平均值,首先累加结果必须正确,所以可以在求累加和地方再设一个断点,命令为:
(6)查看所设断点信息,命令为:
(7)运行程序,命令为:
(8)程序会在所设置的第一个断点处停止,显示:
Breakpoint 1,average_(array=0xbffff44c,min=0xbfff468,max=0xbffff46c,
num=5) at bugging.c:4
4 int i,s=0;
这时,可单步运行程序,命令为:
(9)查看min和max指针所指内容,命令为:
(10)这时,还可以查看一下数组内容,要查看数组5个单元的内容,命令为:
(11)继续运行,继续运行命令为:
(12)程序会在所设置的第二个断点处停止,显示:
Breakpoint 2,average_(array=0xbffff44c,min=0xbfff468,max=0xbffff46c,
num=5) at bugging.c:13
13 s+=array[i];
这时,可单步运行程序,查看累加和附近程序执行情况,并查看相关变量的内容,找到出错点。
出错点为:
S的值不对.
(13)清除所设断点,命令为:
(14)退出gdb,命令为:
五、实验报告要求
1.以书面形式记录下你操作的每一步过程,包括编译及调试过程。
2. 回答所提出的问题。
3.总结上机调试过程中所遇到的问题和解决方法及感想。