工具参考书之GCC基本使用


GCC介绍

  • GCC 原名为 GNU C 语言编译器(GNU C Compiler),因为它原本只能处理 C语言。GCC 很快地扩展,变得可处理 C++。后来又扩展能够支持更多编程语言,如Fortran、Pascal、Objective-C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件(GNU Compiler Collection)。

  • GCC 对于操作系统平台及硬件平台支持,概括起来就是一句话:无所不在。Arm,X86_64等都有GCC的身影。


基本说明

   gcc 编译c文件
   g++ 编译cpp文件

  .c      C源程序;预处理,编译,汇编
  .cpp    C++源程序;预处理,编译,汇编
  .cc     C++源程序;预处理,编译,汇编
  .cxx    C++源程序;预处理,编译,汇编
  .m      Objective-C源程序;预处理,编译,汇编
  .i      预处理后的C文件;编译,汇编
  .ii     预处理后的C++文件;编译,汇编
  .s      汇编语言源程序;汇编
  .S      汇编语言源程序;预处理,汇编
  .h      预处理器文件;通常不出现在命令行上

基本使用

生成可执行文件

-o 说明
gcc main.c -o main c文件用gcc生成exe
g++ main.cpp -o main cpp文件用gcc生成exe
g++ main.cpp -o main cpp文件用gcc生成exe
g++ main.cpp src.cpp -o main 直接编译链接*.cpp,生成main.exe

生成obj文件

-c 说明
g++ -c main.cpp 编绎cpp文件,输出目标main.o文件

生成静态库(.a)

ar -crv 说明
ar -crv libmain.a main.o 由.o生成静态库.a文件(也称归档文件)

生成动态库(.so)

-shared -fPIC 说明
g++ -shared -fPIC -o libmain.so main.o 由.o文件生成动态库.so文件

添加头文件目录

参数 示例 说明
-I g++ main.cpp -I “./src” 头文件在 “./src” 中,多个文件夹用多个 “-I”
-isystem g++ main.cpp -isystem “/usr/inc” 添加/usr/inc;搜索顺序 “-I >= -isystem >= std”

添加库文件

参数 示例 说明
-L g++ main.cpp -o main -L “./lib” 指定库文件路径”./lib”
-l g++ main.cpp -o main -llibmain 指定库文件libmain.so或libmain.a

配置选项

设置源文件编码和可执行文件编码

参数 示例 说明
-finput-charset" g++ main.cpp -finput-charset=utf-8 main.cpp文件编码为utf-8
-fexec-charset" g++ main.cpp -o main -fexec-charset=gbk main.exe字符编码为gbk

设置c++标准

-std 说明
g++ main.cpp -std=c++11 采用c++11标准编译

设置x86或x64

参数 示例 说明
-m32 g++ main.cpp -m32 设置为x86程序
-m64 g++ main.cpp -m64 设置为x64程序

头文件依赖关系

-M
# 生成.h文件的依赖关系,包括标准库头文件
-MM
# 生成.h文件的依赖关系,不包括标准库头文件
# -M和-MM默认包含了-E参数,-E用于使gcc在预处理结束后就停止编译
-MG
# 要求把缺失的头文件按存在对待,并且假定他们和源程序文件在同一目录下;
# 注意:-MG必须和-M或-MM选项一起用;
# 简单来说,-MG用于生成完整的依赖关系(即使头文件还没创建);
# 若确实要编译.c文件,则不应加-MG,因为-M或-MM均默认加-E参数,使得gcc在预处理结束后就停止编译了,自然不会生成.o文件。
-MD
-MMD
# 和-M,-MM类似,但是把依赖关系输出到文件中
-MF <dep>.d
# 设置保存依赖关系的.d的文件的名称
-MT <obj>.o
# 设置依赖关系的目标
-MP
生成的依赖文件里面,依赖规则中的所有.h依赖项都会在该文件中生成一个伪目标,其不依赖任何其他依赖项。该伪规则将避免删除了对应的头文件而没有更新Makefile文件去匹配新的依赖关系而导致make出错的情况出现。

GDB使用

首先将GDB看成一个可以独立的软件,不再是像各种IDE一样,将编译、运行、调试都集成到一个软件中。
在这里介绍GDB命令的基本使用。gdb是一个命令交互试界面,所以需要使用一些命令来对exe进行调试。

附:以下命令没有特殊说明,均在gdb环境中键入运行。
附:如果想使用一个相对方便点的终端界面,可以使用gdb-dashboard,效果图如下:

打开

示例 说明
gdb 此命令在终端下运行,打开gdb
gdb <exec-file> 此命令在终端下运行,打开gdb,并加载可执行文件
gdb -tui 此命令在终端下运行,使用gdb终端界面,可以显示源码
help <cmd> 显示gdb中cmd命令的帮助
file <exec-file> 加载可执行程序文件,以便调试,可执行程序用gcc编译时,需要加-g参数
quit 退出gdb,可简写成q

源代码显示

示例 说明
list 显示当前行到之后10行源代码,可简写成l
l n1,n2 显示n1行到n2行的源代码
l +/-ofs 显示当前行到正/负偏移量的源代码,仿移量为ofs
l <filename:line-num/function> 显示某个文件的指定行号/指定函数
filename包括后缀名,省略则为当前文件

断点

示例 说明
break <filename:line-num/function> <condition> 在行号/函数处下断点
break可简写成b
filename可省略,代表当前文件
b 10 if tmp>10 当变量tmp>10时,在此断点
delete, d 删除所有断点
d N 删除N号断点,(使用info b查看断点信息)

显示所调试程序的信息

示例 说明
display <var>
disp <var>
每次中断时,显示变量var的值
disp <expr> 每次中断时,显示表达式的值
如: disp (float)test/2.0
undisp N 取消显示,N为需要取消变量或表达式的编号(使用info disp查看)
print <var>
p /arg <var>
p /arg <expr>
显示变量或表达式的值,不会每次中断显示
p <var=?> 改变变量的值并显示
arg参数为显示格式,可省略:
  /x : 按十六进制格式显示变量。
  /d : 按十进制格式显示变量。
  /u : 按十六进制格式显示无符号整型。
  /o : 按八进制格式显示变量。
  /t : 按二进制格式显示变量。
  /a : 按十六进制格式显示变量。
  /c : 按字符格式显示变量。
  /f : 按浮点数格式显示变量。

显示gdb程序的信息

示例 说明
info b, i b 显示所有断点信息
i disp 显示所有disp信息
i source, i s 显示当前所在语言件和行号
i variables,i va 显示所有的全局变量和变静态变量名称

运行

示例 说明
run <args>
r <args>
运行程序,直至遇到断点
run后面可接参数,即传给main函数的参数
continue
c
断续执行,直至下一个断点
step <N>
s <N>
执行一行源代码,若有函数,则进入函数
N表示执行N次step
next <N>
n <N>
执行一行源代码,若有函数,则函数一并执行,不会进入函数
N表示执行N次next
nexti/stepi
ni/si
针对汇编指令的step和next
finish
f
执行完当前函数,返回到调用它的函数(包括main函数)
[enter] 直接回车,则执行上一次的命令(这样单步调试时,就不用一直输s/n了)

附调试相关

cygwin调试stackdump

objdump -D -S a.exe > a.rasm
# 反汇编a.exe
# -d: 简单反汇编
# -D: 对所有的section反汇编
# -S: 同时显示源代码和反汇编代码(gcc编译需要-g参数)
# -C: 逆向解析C++符号(C++因为有函数重载的源因,编译中的函数名有变化)
# -l: 显示源文件名和行号
# -j: 指定需要反汇编的section
# -b: 指定a.exe的格式
# -m: 指定a.exe的平台架构

cygwin的stackdump只提供了程序在coredump时的函数调用堆栈信息,如下所示:

Stack trace:
Frame        Function    Args
000FFFFC0D0  00180061979 (00000000000, 000000E0000, 00000000000, 000FFFFDE50)
000FFFFDE50  0018006393C (00000000064, 00000000000, 00000000000, 00000000000)
000FFFFC7F0  0018013CF17 (00000000000, 00000000000, 00000000000, 00000000000)
000FFFFCBC0  00180163304 (00000000004, 0010040302A, 00000000002, 00000000000)
000FFFFCBC0  0018013950B (00000000000, 0018015CFA1, 000FFFFCA90, 00100403030)
000FFFFCBC0  0018013983F (000FFFFCC70, 00180291A60, 000FFFFCDF0, 000FFFFCBC0)
000FFFFCBC0  001801750C7 (0018022AD70, 0010040300B, 000FFFFCBA0, 00000000000)
000FFFFCBC0  001800D267E (0010040302A, 000FFFFC934, 0018015CD90, 00000000000)
000FFFFCBC0  0018013469B (0010040302A, 000FFFFC934, 0018015CD90, 00000000000)
000FFFFCBC0  001004010B2 (0010040302D, 000FFFFC964, 0018015CD90, 000FFFFCC20)
000FFFFCBF0  001004010F0 (00100403030, 0080003869F, 00000000000, 000FFFFCCD0)
000FFFFCC20  00100401133 (00000000020, FF0700010302FF00, 0018004A7BA, 00180049800)
000FFFFCCD0  0018004A826 (00000000000, 00000000000, 00000000000, 00000000000)
00000000000  00180048353 (00000000000, 00000000000, 00000000000, 00000000000)
000FFFFFFF0  00180048404 (00000000000, 00000000000, 00000000000, 00000000000)
End of stack trace

需要根据Function地址在.rasm文件找到对应的函数(这里001004开头的地址是程序中的)。
函数的调用堆栈是从下向上的顺序,可以定位最后调用的函数地址为001004010B2。

coredump常见原因

  • 内存访问越界:

1.由于使用错误的下标,导致数组访问越界。
2.搜索字符串时,依靠字符串结束符来判断字符串是否结束,但是字符串没有正常的使用结束符。
3.使用strcpy, strcat, sprintf, strcmp,strcasecmp等字符串操作函数,将目标字符串读/写爆。应该使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函数防止读写越界。

  • 多线程程序使用了线程不安全的函数

应该使用下面这些可重入的函数,它们很容易被用错:
asctime_r(3c) 、gethostbyname_r(3n) 、getservbyname_r(3n)、ctermid_r(3s) 、gethostent_r(3n) 、getservbyport_r(3n)、 ctime_r(3c) 、getlogin_r(3c)、getservent_r(3n) 、fgetgrent_r(3c) 、getnetbyaddr_r(3n) 、getspent_r、(3c)fgetpwent_r、(3c) getnetbyname_r(3n)、 getspnam_r(3c)、 fgetspent_r(3c)、getnetent_r(3n) 、gmtime_r(3c)、 gamma_r(3m) 、getnetgrent_r(3n) 、lgamma_r(3m) 、getauclassent_r(3)、getprotobyname_r(3n) 、localtime_r(3c) 、getauclassnam_r(3) 、etprotobynumber_r(3n)、nis_sperror_r(3n) 、getauevent_r(3) 、getprotoent_r(3n) 、rand_r(3c) 、getauevnam_r(3)、getpwent_r(3c) 、readdir_r(3c) 、getauevnum_r(3) 、getpwnam_r(3c) 、strtok_r(3c)、 getgrent_r(3c)、getpwuid_r(3c) 、tmpnam_r(3s) 、getgrgid_r(3c) 、getrpcbyname_r(3n)、 ttyname_r(3c)、getgrnam_r(3c) 、getrpcbynumber_r(3n) 、gethostbyaddr_r(3n) 、getrpcent_r(3n)

  • 多线程读写的数据未加锁保护

对于会被多个线程同时访问的全局数据,应该注意加锁保护,否则很容易造成coredump。

  • 非法指针

1.使用空指针;
2.随意使用指针转换。一个指向一段内存的指针,除非确定这段内存原先就分配为某种结构或类型,或者这种结构或类型的数组,否则不要将它转换为这种结构或类型的指针,而应该将这段内存拷贝到一个这种结构或类型中,再访问这个结构或类型。这是因为如果这段内存的开始地址不是按照这种结构或类型对齐的,那么访问它时就很容易因为bus error而coredump。

  • 堆栈溢出

不要使用大的局部变量(因为局部变量都分配在栈上),这样容易造成堆栈溢出,破坏系统的栈和堆结构,导致出现莫名其妙的错误。

函数调用

  • 几个寄存器
ss 栈的段地址寄存器
sp 栈的偏移地址寄存器
bp 基址指针寄存器(默认的段地址为ss)

ss:sp 表示当前的栈顶地址
ss:(bp + n) 表示从栈中访问数据
cs:ip 表示当前的代码执行地址
  • caller调用func

在caller中会执行:

push arg2
push arg1
push ret

在func中会执行:

push bp
mov  bp, sp
push var1
push var2

函数调用堆栈:

| 栈底         | 高位地址
| ....         |
| arg2         |
| arg1         |
| ret          |
| caller中bp值 | <- sp   :sp指向bp所在的地址 ('push bp')
| var1         |           且func中的bp=sp    ('mov bp, sp')
| var2         |
| ....         | 低位地址

对于func来说:
  ss:(bp + n)   bp向上获取func的返回地址和参数
  ss:(bp - n)   bp向下得到func的局部变量
  ss:bp         bp本身保存着caller中的bp值
                使用ss:(ss:(bp ± n))可以获取caller的返回地址、参数、局部变量等

这里只是简声描述下基本的堆栈数据,详细的涉及了其它的bx、di等现场数据。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [ yehuohan@gmail.com ]

文章标题:工具参考书之GCC基本使用

本文作者:Y

发布时间:2017-06-07, 01:05:36

最后更新:2020-07-16, 13:49:54

原始链接:http://yehuohan.github.io/2017/06/07/%E7%AC%94%E8%AE%B0/DOC/%E5%B7%A5%E5%85%B7%E5%8F%82%E8%80%83%E4%B9%A6%E4%B9%8BGCC%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。