C开发编译与调试

Posted mChenys

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C开发编译与调试相关的知识,希望对你有一定的参考价值。

目录

一、gcc编译流程

1.1 预处理阶段

宏定义展开,宏定义替换,展开include的文件

gcc -E -o hello.i hello.c

例如源文件hello.c内容如下:

#include <stdio.h>
int main()

	printf("hello test");
	return 0;

经过预处理后生成的hello.i内容如下:

# 1 "hello.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 391 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "hello.c" 2
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h" 1 3 4
# 64 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h" 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h" 1 3 4
# 68 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h" 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h" 1 3 4
# 649 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h" 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_symbol_aliasing.h" 1 3 4
# 650 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h" 2 3 4
# 715 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h" 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_posix_availability.h" 1 3 4
# 716 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h" 2 3 4
# 69 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h" 2 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/Availability.h" 1 3 4
# 135 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/Availability.h" 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/AvailabilityVersions.h" 1 3 4
# 136 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/Availability.h" 2 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/AvailabilityInternal.h" 1 3 4
# 137 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/Availability.h" 2 3 4
# 70 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h" 2 3 4

# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_types.h" 1 3 4
# 27 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_types.h" 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types.h" 1 3 4
# 33 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types.h" 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/machine/_types.h" 1 3 4
# 34 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/machine/_types.h" 3 4
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/arm/_types.h" 1 3 4
# 13 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/arm/_types.h" 3 4
typedef signed char __int8_t;



typedef unsigned char __uint8_t;
typedef short __int16_t;
typedef unsigned short __uint16_t;
typedef int __int32_t;
typedef unsigned int __uint32_t;
typedef long long __int64_t;
typedef unsigned long long __uint64_t;

///内容太长了....这里省略....

typedef long __darwin_intptr_t;
typedef unsigned int __darwin_natural_t;
# 46 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/arm/_types.h" 3 4
typedef int __darwin_ct_rune_t;

# 2 "hello.c" 2
int main()

 printf("hello test");
 return 0;


可以看到预处理阶段后,源码的内容变多了,因为这里面包含了宏定义和include的文件的内容

1.2 预编译阶段

在这个阶段,gcc才会去检测代码的规范,语法是否有错误,gcc会把代码翻译成汇编

gcc -S -o hello.s hello.i

生成的汇编代码在hello.s文件内

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 11, 0	sdk_version 11, 3
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #32                     ; =32
	stp	x29, x30, [sp, #16]             ; 16-byte Folded Spill
	add	x29, sp, #16                    ; =16
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	mov	w8, #0
	str	w8, [sp, #8]                    ; 4-byte Folded Spill
	stur	wzr, [x29, #-4]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	ldr	w0, [sp, #8]                    ; 4-byte Folded Reload
	ldp	x29, x30, [sp, #16]             ; 16-byte Folded Reload
	add	sp, sp, #32                     ; =32
	ret
	.cfi_endproc
                                        ; -- End function
	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"hello test"

.subsections_via_symbols

1.3 汇编阶段(最耗时)

把.s文件翻译成二进制.o文件(机器指令)

gcc -c -o hello.o hello.s

1.4 链接阶段

计算逻辑地址,合并数据段,有些函数是在另外一个so库文件中的

gcc -o hello hello.o

经过这一步之后就会生成可执行文件hello了.然后在控制台上输入./hello就可以执行了.

通常用的比较多的就是-o指定生成可执行文件的名字, 例如gcc -o hello hello.c就可以生成可执行文件hello了

二、gcc的相关参数

  • -I参数
    -I (大写的i):用于指定当前的.c文件用到的.h文件所在的目录,这样就不会报找不到头文件的错误了.
    例如某个目录下有一个a.h文件
#pragma once

int sum(int a,int b); //声明sum方法

然后hello.c文件如下:

#include<stdio.h>
#include "a.h" //包含a.h头文件

int main()

	int ret = sum(1,2);
	printf("ret=%d\\n",ret);
	return 0;


int sum(int a,int b) //实现sum方法

	return a+b;


如果直接用gcc hello.c会报这个错误

然后使用-I指定a.h的所在的目录

 gcc hello.c -I ../12/

这样编译就OK了

  • -o选项
    用于指定编译后生成的文件名称,不指定的话默认是a.out

  • -D选项
    通过-D可以在编译的时候设置宏定义
    例如hello.c的代码如下:

#include<stdio.h>
#include "a.h"

int main()

	int ret = sum(1,2);
	#ifdef DEBUG
		printf("hello world\\n");
	#endif
		printf("ret=%d\\n",ret);
	return 0;


int sum(int a,int b)

	return a+b;

上面代码的意思就是如果存在DEBUG宏,那么就会多打印hello world
通过下面命令可以编译的时候添加宏定义

  • -L选项
    用于指定包含库的路径

  • -l选项(小写的L)
    用于指定库的名(通常是libxxx.so或者libxxx.a 使用时是-lxxx)

  • -g选项
    用于gdb调试,不加此选项不能调试

  • -Wall选项
    用于显示更多提示

  • -lstdc++选项
    用于编译c++文件,当然c++文件一般用g++编译.

  • -O选项
    优化代码的选项,有1-3级别,例如-O1表示用1级别优化

  • -c选项
    将.c文件或.s汇编文件编译生成.o二进制文件

  • -fPIC选项
    将.c文件编译成与位置无关的代码的.o文件 (PIC是Position Independent Code的简写)

  • -shared
    将与位置无关代码的.o文件打包成.so动态库文件.

三、Linux下静态库的制作和使用

3.1 制作静态库

静态库的命名规范为:libXxx.a 对应Windows的.lib文件.
制作步骤:
a.通过gcc -c选项将.c文件编译成.o文件
b.使用ar rcs 命令将.o文件打包,使用命令:

ar  rcs   libxxx.a   file1.o file2.o ...

假设当前工程的.c和.h文件结构如下

其中a,b,d,e的c文件都包含了include目录下的.h文件,那么打包静态库的步骤如下:
首先,将所有的.c文件编译成.o文件

用-I(大写的i)指定头文件的路径,用*.c代替src目录下的所有.c文件, 当然你不嫌麻烦的话也可以逐个进行.o文件的编译.然后,将所有的.o文件进行打包,库名叫做libCalc.a

同样,如果你不嫌麻烦也可以这样打包: ar rcs libCalc.a a.o b.o d.o e.o

如何查看.a库文件的内容呢?
通过nm命令可以查看.a文件内包含了哪些.o文件,以及用到了什么函数,例如:

3.2 使用静态库

将上面步骤生层的libCalc.a文件移动到工程的lib目录内,同时在工程的根目录下创建main.c文件,内容如下:

#include <stdio.h>
#include "head.h"

int main()

	int ret1 = add(10,10);
	int ret2 = mul(10,10);
	int ret3 = div(10,10);
	int ret4 = sub(10,10);
	printf("10+10=%d\\n",ret1);
	printf("10*10=%d\\n",ret2);
	printf("10/10=%d\\n",ret3);
	printf("10-10=%d\\n",ret4);

	return 0;


此时的工程目录结构如下:

然后编译main.c文件的时候需要输入下面命令

gcc -o main main.c -I ./include/ -L ./lib/ -lCalc


然后输入./main就可以运行程序了.

ps:使用-l的时候,格式是固定的,库文件需要省略前缀lib和后缀.a,并且需要和-l(小写的L)连着写.

四、Linux下动态库的制作和使用

4.1 制作动态库

动态库的命名规范为:libXxx.so 对应Windows的.dll文件.

制作步骤:
a.通过gcc -c -fPIC 将.c文件编译与位置无关的代码的.o文件,关键参数 -fPIC (PIC是Position Independent Code的简写)
b.通过gcc -shared -o将位置无关的.o文件打包.so文件,so文件的命名格式:libXxx.so

ps:上面2个步骤也可以合成一个步骤,就是 -fPIC 和 -shared 一起使用

例如还是上面那个工程

下面开始制作动态库
首先,将.c文件生成位置无关代码的.o文件,进入src目录,使用下面命令

gcc -c *.c -fPIC -I ../include/


然后,将所有.o文件打包成一个.so文件,使用下面命令

gcc -shared -o libCalc.so *.o


然后将生成的libCalc.so文件拷贝到当前工程的lib目录内, 然后在工程根目录,使用下面命令进行编译生成可执行程序

gcc -o main main.c -I include/ -L lib/ -lCalc


此时的项目结构:

现在你尝试./main运行会发现运行出错,并且使用ldd命令查看main会发现libCalc.so文件找不到

这时就需要介绍如何使用动态库了

4.2 使用动态库

方式一,将libCalc.so文件直接拷贝到系统的/lib或者/usr/lib目录内,或者创建软连接(不推荐)

方式二,将库路径添加到环境变量 LD_LIBRARY_PATH中(这种方式是临时的,下次登录就没了,不推荐),使用如下命令添加

export LD_LIBRARY_PATH=/home/chenys/work/WorkSpace/C/14/day01/lib:$LD_LIBRARY_PATH

当然这种方式还可以将配置添加到home目录下的.bashrc文件中,这样就是永久的.

方式三,配置/etc/ld.so.conf文件,在文件末尾增加库路径,例如/home/chenys/work/WorkSpace/C/14/day01/lib (ps:路径随你定,只要有.so文件存在这个路径即可.)

然后执行sudo ldconfig -v刷新下配置.

配置完成后,就可以运行main程序了,并且用ldd命令也能查看到libCalc.so正确找到了.

五、makefile的编写

makefile的好处是一次编写,终身受益
a.命名规则:makefile 或者 Makefile
b.三要素: 目标,依赖,规则, 其中目标是必须的,其它2个非必须.
c.写法:

目标:依赖
tab键规则命令

d.如何执行makefile文件:
控制台输入make [目标] ,如果不写目标,那么默认执行makefile的第一个目标,带上目标后则执行指定的目标

5.1 创建makefile生成模板

除了手动创建makefile文件外,我们还可以拷贝makefile的模板来生成,为了操作方便,我这里通过在~/.bashrc文件中添加一个alias命令来简化这一过程,添加如下语句到~/.bashrc文件中:
alias echomake='cat ~/template/makefile.template >> makefile' 注意等号2边不要留有空白符,然后在创建~/template/makefile.template文件

SrcFiles=$(wildcard *.c)

TargetFiles=$(patsubst %.c,%,$(SrcFiles))

all:$(TargetFiles)

%:%.c
	gcc -o $@ $< -g

.PHONY:clean

clean:
	-@rm -rf $(TargetFiles)

最后在控制台输入echomake就可以自动使用模板创建一个makefile了

5.2 使用演示

假设工程的目录结构如下

其中head.h的内容如下:

int add(int a,int b);
int mul(int a,int b);
int div(int a,int b);
int sub(int a,int b);

a.c、b.c、d.c、e.c的内容如下:

//a.c
#include "../include/head.h"

int add(int a,int b)
    return a+b;


//b.c
#include "../include/head.h"

int mul(int a,int b)
    return a+b;


//d.c
#include "../include/head.h"

int div(int a,int b)
    return a+b;


//e.c
#include "../include/head.h"

int sub(int a,int b)
    return a+b;


main.c内容如下:

#include <stdio.h>
#include "include/head.h"

int main(int argc,char* args[])

	if(argc >1)
	
		for(int i=1;i<argc;i++)
		
			printf("第%d个参数=%s\\n",i,args[i]);
		
	
	int ret1 = add(10,10);
	int ret2 = mul(10,10);
	int ret3 = div(10,10);
	int ret4 = sub(10,10);
	printf("10+10=%d\\n",ret1);
	printf("10*10=%d\\n",ret2);
	printf("10/10=%d\\n",ret3);
	printf("10-10=%d\\n",ret4);

	return 0;

然后在工程根目录下通过vi makefile打开编辑文本,内容例如:

main:main.c ./src/a.c ./src/b.c ./src/d.c ./src/e.c
	gcc -o main -I ./include main.c ./src/a.c ./src/b.c ./src/d.c ./src/e.c

注意命令前是一个tab键
保存退出后,输入make命令会执行makefile的内容.

可以看到make执行的内容,就是makefile定义的规则,同时在当前目录下生层了main可执行文件.
如果执行后看到Makefile:2:*** missing separator. Stop.错误,那么很有可能是你的makefile文件在命令前的tab键格式不对,需要在vi编辑模式下按下tab,不要自己打空格.

上面这种编写方式的缺点是,如果更改其中一个文件,那么所有的源码都需要重新编译.

5.3 改进makefile文件的编写

考虑采用编译过程分解,先生成.o文件,然后使用.o文件得到结果.

修改项目结构,同时修改a.c、b.c、d.c、e.c里面的include路径

在项目根目录下重新创建一个makefile文件

main:main.o a.o b.o d.o e.o
	gcc -o main -I ./include main.o a.o b.o d.o e.o
main.o:main.c
	gcc -c main.c -I ./include
a.o:a.c
	gcc -c a.c -I ./include 
b.o:b.c
	gcc -c b.c -I ./include 
d.o:d.c
	gcc -c d.c -I ./include 
e.o:e.c
	gcc -c e.c -I ./include 

保存后,输入make,等到下面结果

执行main,得到结果.

这样看似和之前的没啥区别, 但是当我们修改a.c代码后, 再次执行make命令,你会发现输出的命令变少了

可以看到仅输出了2条,第一条是生成a.o的; 第2条是生成main可执行程序的, 这样就实现了增量编译了.效率也得到了提升.

六、makefile的语法

  • 1)定义变量
    变量名=值

  • 2)注释
    #号表示注释

  • 3)使用变量
    $(变量名)

例如:

#定义一个变量,存放依赖的路径
objFile = main.o a.o b.o d.o e.o
#通过&(变量名)来使用变量
main:$(objFile)
	gcc -o main -I ./include main.o a.o b.o d.o e.o
main.o:main.c
	gcc -c main.c -I ./include
a.o:a.c
	gcc -c a.c -I ./include 
b.o:b.c
	gcc -c b.c -I ./include 
d.o:d.c
	gcc -c d.c -I ./include 
e.o:e.c
	gcc -c e.c -I ./include 
  • 4)函数
    wildcard:可以进行文件匹配,用法: $(wildcard 要查找的文件)
    patsubst:内容替换(翻译:subst 字符串替换 . patsubst 带模式的字符串替换),用法:
    $(patsubst 替换前的规则,替换后的规则,需要替换的内容)

例如:

#查找当前目录下所有的.c文件,保存到SrcFiles变量中
SrcFiles=$(wildcard *.c)
#将SrcFiles变量内的.c后缀修改成.o后缀,然后保存到ObjFiles变量中,这里用到了%通配符
ObjFiles=$(patsubst %.c,%.o,$(SrcFiles))

#main的依赖以及规则中用到的文件名就可以直接使用ObjFiles变量的内容了来写活了.
main:$(ObjFiles)
	gcc -o main -I ./include $(ObjFiles)
main.o:main.c
	gcc -c main.c -I ./include
a.o:a.c
	gcc -c a.c -I ./include 
b.o:b.c
	gcc -c b.c -I ./include 
d.o:d.c
	gcc -c d.c -I ./include 
e.o:e.c
	gcc -c e.c -I ./include
  • 5)模式推导和makefile变量结合使用
    使用%号来代替符合一定规律的内容,相当于通配符, 通常会和makefile的变量一起使用,常用的makefile变量有:
$@ :代表目标
$^ :代表全部依赖
$< :

以上是关于C开发编译与调试的主要内容,如果未能解决你的问题,请参考以下文章

gdb调试工具

第01课:调试信息与调试原理

VER是啥意思啊!?

C语言编译器&集成开发工具

C语言的编译与调试

gdb调试工具常用命令 && kdb