前言:开源(Open Source,开放源码)被非盈利软件组织(美国的Open Source Initiative协会)注册为认证标记,并对其进行了正式的定义,用于描述那些源码可以被公众使用的软件,并且此软件的使用、修改和发行也不受许可证的限制。
开源项目的所有者不属于任何组织或个人。在遵守开源协议的条件下,开源产品可通过修改代码定制成属于自己的个性化产品。
一、Linux kernel
Linux内核是一个操作系统(OS)内核,本质上定义为类Unix。它用于不同的操作系统,主要是以不同的Linux发行版的形式。Linux内核是第一个真正完整且突出的免费和开源软件示例。Linux 内核是第一个真正完整且突出的免费和开源软件示例,促使其广泛采用并得到了数千名开发人员的贡献。
Linux 内核由芬兰赫尔辛基大学的学生 Linus Torvalds 于 1991 年创建。随着程序员调整其他自由软件项目的源代码以扩展内核的功能,它迅速取得了进展。Torvalds 首先使用 80386 汇编语言编写的任务切换器以及终端驱动程序,然后将其发布到 Comp.os.minix Usenet 组。它很快被 Mini社区所改编,为该项目提供了见解和代码。
Linux 内核越来越受欢迎,因为 GNU 自己的内核 GNU Hurd 不可用且不完整,而 Berkeley Software DistribuTIon(BSD)操作系统仍然受到法律问题的困扰。在开发人员社区的帮助下,Linux 0.01 于 1991 年 9 月 17 日发布。
Linux最早是由芬兰 Linus Torvalds为尝试在英特尔x86架构上提供自由的类Unix操作系统而开发的。该计划开始于1991年,在计划的早期有一些 Minix 黑客提供了协助,而如今全球无数程序员正在为该计划无偿提供帮助。
1.1内核结构
操作系统是一个用来和硬件打交道并为用户程序提供一个有限服务集的低级支撑软件。一个计算机系统是一个硬件和软件的共生体,它们互相依赖,不可分割。计算机的硬件,含有外围设备、处理器、内存、硬盘和其他的电子设备组成计算机的发动机。但是没有软件来操作和控制它,自身是不能工作的。完成这个控制工作的软件就称为操作系统,在Linux的术语中被称为“内核”,也可以称为“核心”。Linux内核的主要模块(或组件)分以下几个部分:存储管理、CPU和进程管理、文件系统、设备管理和驱动、网络通信,以及系统的初始化(引导)、系统调用等。
结构属性
在讨论大型而复杂的系统的体系结构时,可以从很多角度来审视系统。体系结构分析的一个目标是提供一种方法更好地理解源代码。
Linux 内核实现了很多重要的体系结构属性。在或高或低的层次上,内核被划分为多个子系统。Linux 也可以看作是一个整体,因为它会将所有这些基本服务都集成到内核中。这与微内核的体系结构不同,后者会提供一些基本的服务,例如通信、I/O、内存和进程管理,更具体的服务都是插入到微内核层中的。
随着时间的流逝,Linux 内核在内存和 CPU 使用方面具有较高的效率,并且非常稳定。但是对于 Linux 来说,最为有趣的是在这种大小和复杂性的前提下,依然具有良好的可移植性。Linux 编译后可在大量处理器和具有不同体系结构约束和需求的平台上运行。一个例子是 Linux 可以在一个具有内存管理单元(MMU)的处理器上运行,也可以在那些不提供MMU的处理器上运行。Linux 内核的uClinux移植提供了对非 MMU 的支持。
开发规范
核心的开发和规范一直是由Linux社区控制着,版本也是不重复的。实际上,操作系统的内核版本指的是在Linus本人领导下的开发小组开发出的系统内核的版本号。自1994年3月14日发布了第一个正式版本Linux 1.0以来,每隔一段时间就有新的版本或其修订版公布。
Linux将标准的GNU许可协议改称Copyleft,以便与Copyright相对照。通用的公共许可(GPL)允许用户销售、拷贝和改变具有Copyleft的应用程序。当然这些程序也可以是Copyright的,但是你必须允许进一步的销售、拷贝和对其代码进行改变,同时也必须使他人可以免费得到修改后的源代码。事实证明,GPL对于Linux的成功起到了极大的作用。它启动了一个十分繁荣的商用Linux阶段,还为编程人员提供了一种凝聚力,诱使大家加入这个充满了慈善精神的Linux运动。
1.2主要子系统
系统调用接口
SCI 层提供了某些机制执行从用户空间到内核的函数调用。正如前面讨论的一样,这个接口依赖于体系结构,甚至在相同的处理器家族内也是如此。SCI 实际上是一个非常有用的函数调用多路复用和多路分解服务。在 ./linux/kernel 中您可以找到 SCI 的实现,并在 ./linux/arch 中找到依赖于体系结构的部分。
进程管理
进程管理的重点是进程的执行。在内核中,这些进程称为线程,代表了单独的处理器虚拟化(线程代码、数据、堆栈和 CPU寄存器)。在用户空间,通常使用进程 这个术语,不过 Linux 实现并没有区分这两个概念(进程和线程)。内核通过 SCI 提供了一个应用程序编程接口(API)来创建一个新进程(fork、exec 或 Portable Operating System Interface [POSⅨ] 函数),停止进程(kill、exit),并在它们之间进行通信和同步(signal 或者 POSⅨ 机制)。
进程管理还包括处理活动进程之间共享 CPU 的需求。内核实现了一种新型的调度算法,不管有多少个线程在竞争 CPU,这种算法都可以在固定时间内进行操作。这种算法就称为 O⑴ 调度程序,这个名字就表示它调度多个线程所使用的时间和调度一个线程所使用的时间是相同的。O⑴ 调度程序也可以支持多处理器(称为对称多处理器或 SMP)。您可以在 ./linux/kernel 中找到进程管理的源代码,在 ./linux/arch 中可以找到依赖于体系结构的源代码。
内存管理
VFS 在用户和文件系统之间提供了一个交换层,内核所管理的另外一个重要资源是内存。为了提高效率,如果由硬管理虚拟内存,内存是按照所谓的内存页 方式进行管理的(对于大部分体系结构来说都是 4KB)。Linux 包括了管理可用内存的方式,以及物理和虚拟映射所使用的硬件机制。
不过内存管理要管理的可不止 4KB缓冲区。Linux 提供了对 4KB缓冲区的抽象,例如 slab分配器。这种内存管理模式使用 4KB缓冲区为基数,然后从中分配结构,并跟踪内存页使用情况,比如哪些内存页是满的,哪些页面没有完全使用,哪些页面为空。这样就允许该模式根据系统需要来动态调整内存使用。
为了支持多个用户使用内存,有时会出现可用内存被消耗光的情况。由于这个原因,页面可以移出内存并放入磁盘中。这个过程称为交换,因为页面会被从内存交换到硬盘上。内存管理的源代码可以在 ./linux/mm 中找到。
虚拟文件系统
虚拟文件系统(VFS)是 Linux 内核中非常有用的一个方面,因为它为文件系统提供了一个通用的接口抽象。VFS 在 SCI 和内核所支持的文件系统之间提供了一个交换层。
VFS 在用户和文件系统之间提供了一个交换层,在 VFS 上面,是对诸如 open、close、read 和 write 之类的函数的一个通用 API 抽象。在 VFS 下面是文件系统抽象,它定义了上层函数的实现方式。它们是给定文件系统(超过 50 个)的插件。文件系统的源代码可以在 ./linux/fs 中找到。
文件系统层之下是缓冲区缓存,它为文件系统层提供了一个通用函数集(与具体文件系统无关)。这个缓存层通过将数据保留一段时间(或者随即预先读取数据以便在需要是就可用)优化了对物理设备的访问。缓冲区缓存之下是设备驱动程序,它实现了特定物理设备的接口。
二、Bash命令行解释器
Bash是一种Unix/Linux操作系统的命令行解释器(shell),也是Linux系统中默认的shell。Bash提供了一个命令行界面,用户可以在其中输入各种命令,并与计算机交互。通过Bash,用户可以使用各种工具来管理文件和目录、编辑文本文件、启动程序等等。
同时,Bash还支持脚本编程,可以编写各种自动化脚本来完成一些任务。这是一个开源 GNU 项目。它提供了比 Bourne Shell 更好的功能,适用于编程和交互使用。 我们可以这么理解,Bash 是一个命令处理器,通常运行于文本窗口中,可以将用户输入的命令解释并执行相应的操作,这样式的文件被称作脚本。 Bash 是绝大多数 Linux 、MAC 及 OS 默认的 shell 程序,并且 Shell Script 都大致相同。当您学会一种 Shell 后,其它的 Shell 都能够很快上手,而且一种 Shell Script 通常可以在很多 Shell 上使用,因此您不必在学习哪种 Shell 的选择上耗费太多的时间。
Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。Bash还能从文件中读取命令,这样的文件称为脚本。和其他Unix shell 一样,它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。包括关键字、语法在内的基本特性全部是从sh借鉴过来的。其他特性,例如历史命令,是从csh和ksh借鉴而来。总的来说,Bash虽然是一个满足POSIX规范的shell,但有很多扩展。
2.1语法与特性
bash的命令语法是Bourne shell命令语法的超集。数量庞大的Bourne shell脚本大多不经修改即可以在bash中执行,只有那些引用了Bourne特殊变量或使用了Bourne的内置命令的脚本才需要修改。bash的命令语法很多来自Korn shell(ksh)和C shell(csh),例如命令行编辑,命令历史,目录栈,$RANDOM和$PPID变量,以及POSIX的命令置换语法:$(...)。作为一个交互式的shell,按下TAB键即可自动补全已部分输入的程序名,文件名,变量名等等。
使用'function'关键字时,Bash的函数声明与Bourne/Korn/POSIX脚本不兼容(Korn shell 有同样的问题)。不过Bash也接受Bourne/Korn/POSIX的函数声明语法。因为许多不同,Bash脚本很少能在Bourne或Korn解释器中运行,除非编写脚本时刻意保持兼容性。然而,随着Linux的普及,这种方式正变得越来越少。不过在POSIX模式下,Bash更加符合POSIX。bash的语法针对Bourne shell的不足做了很多扩展。其中的一些列举在这里。
花括号扩展
花括号扩展是一个从C shell借鉴而来的特性,它产生一系列指定的字符串(按照原先从左到右的顺序)。这些字符串不需要是已经存在的文件。
$ echo a{p,c,d,b}eape ace ade abe$ echo {a,b,c}{d,e,f}ad ae af bd be bf cd ce cf
花括号扩展不应该被用在可移植的shell脚本中,因为Bourne shell产生的结果不同。
#! /bin/sh # 传统的shell并不产生相同结果echo a{p,c,d,b}e # a{p,c,d,b}e
当花括号扩展和通配符一起使用时,花括号扩展首先被解析,然后正常解析通配符。因此,可以用这种方法获得当前目录的一系列JPEG和PEG文件。
ls *.{jpg,jpeg,png} # 首先扩展为*.jpg *.jpeg *.png,然后解析通配符 echo *.{png,jp{e,}g} # echo显示扩展结果;花括号扩展可以嵌套。
除了列举备选项,还可以用“..”在花括号扩展中指定字符或数字范围。较新的Bash版本接受一个整数作为第三个参数,指定增量。
$ echo {1..10}1 2 3 4 5 6 7 8 9 10 $ echo file{1..4}.txtfile1.txt file2.txt file3.txt file4.txt$ echo {a..e}a b c d e$ echo {1..10..3}1 4 7 10 $ echo {a..j..3}a d g j
当花括号扩展和变量扩展一起使用时,变量扩展解析于花括号扩展之后。有时有必要使用内置的eval函数。
$ start=1; end=10 $ echo {$start..$end} # 由于解析顺序,无法得到想要的结果 {1..10} $ eval echo {$start..$end} # 首先进行变量扩展的解析 1 2 3 4 5 6 7 8 9 10
使用整数
与Bourne shell不同的是bash不用另外生成进程即能进行整数运算。bash使用((...))命令和$[...]变量语法来达到这个目的:
VAR=55 # 将整数55赋值给变量VAR ((VAR = VAR + 1)) # 变量VAR加1。注意这里没有'$' ((++VAR)) # 另一种方法给VAR加1。使用C语言风格的前缀自增 ((VAR++)) # 另一种方法给VAR加1。使用C语言风格的后缀自增 echo $((VAR * 22)) # VAR乘以22并将结果送入命令 echo $[VAR * 22] # 同上,但为过时用法
((...))命令可以用于条件语句,因为它的退出状态是0或者非0(大多数情况下是1),可以用于是与非的条件判断:
if((VAR == Y * 3 + X * 2)) then echo Yes fi ((Z > 23)) && echo Yes
((...))命令支持下列比较操作符:'==', '!=', '>', '<', '>=',和'<='。bash不能在自身进程内进行浮点数运算。当前有这个能力的unix shell只有Korn shell和Z shell。
输入输出重定向
bash拥有传统Bourne shell缺乏的I/O重定向语法。bash可以同时重定向标准输出和标准错误,这需要使用下面的语法:
command &> file
这比等价的Bourne shell语法"command > file 2>&1"来的简单。2.05b版本以后,bash可以用下列语法重定向标准输入至字符串(称为here string):
command <<< "string to be read as standard input"
如果字符串包括空格就需要用引号包裹字符串。
例子: 重定向标准输出至文件,写数据,关闭文件,重置标准输出。
# 生成标准输出(文件描述符1)的拷贝文件描述符6 exec 6>&1 # 打开文件"test.data"以供写入 exec 1>test.data # 产生一些内容 echo "data:data:data" # 关闭文件"test.data" exec 1>&- # 使标准输出指向FD 6(重置标准输出) exec 1>&6 # 关闭FD6 exec 6>&-
打开及关闭文件
# 打开文件test.data以供读取 exec 6<test.data # 读文件直到文件尾 while read -u 6 dta do echo "$dta" done # 关闭文件test.data exec 6<&-
抓取外部命令的输出
# 运行'find'并且将结果存于VAR # 搜索以"h"结尾的文件名 VAR=$(find . -name "*h")
进程内的正则表达式
bash 3.0支持进程内的正则表达式,使用下面的语法:
[[ string =~ regex ]]
正则表达式语法同regex(7)man page所描述的一致。正则表达式匹配字符串时上述命令的退出状态为0,不匹配为1。正则表达式中用圆括号括起的子表达式可以访问shell变量BASH_REMATCH,如下:
if [[ abcfoobarbletch =~ 'foo(bar)bl(.*)' ]] then echo The regex matches! echo $BASH_REMATCH -- outputs: foobarbletch echo ${BASH_REMATCH[1]} -- outputs: bar echo ${BASH_REMATCH[2]} -- outputs: etch fi
使用这个语法的性能要比生成一个新的进程来运行grep命令优越,因为正则表达式匹配在bash进程内完成。如果正则表达式或者字符串包括空格或者shell关键字,(诸如'*'或者'?'),就需要用引号包裹。Bash 4 开始的版本已经不需要这么做了。
转义字符
$'string'形式的字符串会被特殊处理。字符串会被展开成string,并像C语言那样将反斜杠及紧跟的字符进行替换。反斜杠转义序列的转换方式如下:
扩展后的结果将被单引号包裹,就好像美元符号一直就不存在一样。双引号包裹的字符串前若有一个美元符号($"...")将会使得字符串被翻译成符合当前locale的语言。如果当前locale是C或者POSIX,美元符号会被忽略。如果字符串被翻译并替换,替换后的字符串仍被双引号包裹。
2.2Shell的用途
Shell:UNIX Shell是一种程序或命令行解释程序,用于解释用户直接输入的用户命令或从文件中读取的用户命令(即,Shall Script),然后将它们传递给操作系统以进行操作或处理。要注意,这个过程是解释而不编译脚本,因为计算机系统会解释它们,并且无需按执行顺序编译Shell脚本。
Linux操作系统中有不同类型的Shell,其中一些如下:
- Bourne Shell
- C shell
- Korn Shell
- GNU Bourne Shell
要想知道操作系统支持哪种Shell类型,可在终端中输入以下命令:
cat /etc/shells
要想知道bash在操作系统中的位置,可键入以下命令,将获得一个特定的位置:
which bash
下面是查看Ubutu支持的Shell类型以及其bash shell所在位置的示例:
bash的优势
- 通过上下方向键来调取过往执行过得Linux命令
- 命令或参数仅需要输入前几位就可以用Tab键补全
- 具有强大的批处理脚本
- 具有实用的环境变量功能
2.3bash的使用
如图,bash可以从标准输入或者文件中读取命令。
1. 标准输入读取命令
如图bash是可以嵌套的。当我们打一个exit就会退出一个bash。多打一个exit的话便会退出SSH连接。
2. 从文件中读取命令
创建sh01.sh文件,输入如下内容:
我们使用source命令来执行我们的sh文件,source是内部命令,具体含义如下:“在当前shell执行文件中的命令”.什么是当前shell?我们说过bash是可以嵌套的。不同的bash执行相同的命令,可能结果不同(比如 echo $$ 来输出当前进程号),所有当前shell就是指的现在所在层的bash。
执行结果如下:
与source命令相同.也表示在当前shell执行文件内的命令:
2.4bash的层级关系
bash具有层级关系,我们可以通过pstree命令来查看bash的层级关系,示例如下:
系统进入默认是第一层bash【1235】,当我们再键入一个bash命令就会嵌套一层bash,依次类推,才有了我们进程号为1798、1805的bash。
思考:既然bash是一个命令,那么我么是否可以bash执行文件内容呢?
当然可以。但是又会嵌套一层bash,具体如下:
但是我们执行pstree命令却发现为什么只有最外面一层默认的父bash,而没有执行sh01.sh文件的子bash呢?
显然这个过程是先开启bash然后执行完毕后再退出bash。
三、Vim文本编辑器
Vim是一款强大的文本编辑器,也是Linux系统下最常用的编辑器之一。它可以运行在多种操作系统平台上,并支持多种文件格式和编程语言。
Vim具有很多特性,如分屏、富文本格式、插件等等,使其非常适合程序员开发工作和日常使用。
它的优点包括:
- 可定制性高:用户可以自定义快捷键、配置文件等。
- 命令模式和插入模式切换方便:可以随时在命令模式和插入模式之间进行切换。
- 强大的搜索功能:支持正则表达式和全局搜索。
- 多种操作方式:支持单个字符、行或整个文档进行复制、剪切、粘贴等操作。
由于Vim使用起来需要一些时间去学习掌握,但一旦熟练掌握后将会显著提高编辑效率。
3.1使用技巧
vim - Vi IMproved, a programmers text editor vim [options] [file ..] vim [options] - vim [options] -t tag vim [options] -q [errorfile]
1)+#: 打开文件后,直接让光标处于第#行的行首
如:vim +20 file
2)+/PATTERN:打开文件后,直接让光标处于第一个被pattren匹配的到的行的字符所在位置
3)vim –d file1 file2… 比较多个文件
example:vim -d test.sh test.txt 类似命令有:vimdiff test.sh test.txt
vim的三大模式:
- 1)命令模式
- 2)插入模式
- 3)末行模式
Esc键可以切换模式,Esc键退出当前模式,Esc键 Esc键总是返回到命令模式
3.2VIM插件管理利器-vundle
安装配置vundle
第一步,创建VIM的目录和配置文件:
在~目录下,添加.vimrc文件和.vim/bundle/vundle目录.
第二步,在.vimrc中添加Vundle的配置内容:
"use vundle to manage plugin filetype off set nocompatible set rtp+=~/.vim/bundle/vundle call vundle#rc()
注:最好是将这部分内容放到此配置文件的最前面
安装插件:
打开VIM,在命令模式下执行
:BundleInstall
等等一会儿就自动安装完成了.
添加插件操作
这个vundle可以自动去相应的官方去查找相应的制作,自动下载,例如,要使用tagbar,则在.vimrc添加
Bundle "majutsushi/tagbar"
然后同样在命令模式下执行下面命令即可自动安装了:
:BundleInstall
常见插件参考:
" Syntax Bundle 'asciidoc.vim' Bundle 'confluencewiki.vim' Bundle 'html5.vim' Bundle 'JavaScript-syntax' "Bundle 'mako.vim' Bundle 'moin.vim' Bundle 'python.vim--Vasiliev' Bundle 'xml.vim' " Color Bundle 'desert256.vim' Bundle 'Impact' Bundle 'vibrantink' Bundle 'vividchalk.vim' " Ftplugin Bundle 'python_fold' " Indent "Bundle 'indent/html.vim' Bundle 'IndentAnything' Bundle 'Javascript-Indentation' Bundle 'mako.vim--Torborg' Bundle 'gg/python.vim' " Plugin Bundle 'The-NERD-tree' Bundle 'AutoClose--Alves' Bundle 'auto_mkdir' Bundle 'cecutil' Bundle 'fcitx.vim' Bundle 'FencView.vim' "Bundle 'FuzzyFinder' Bundle 'jsbeautify' Bundle 'L9' Bundle 'Mark' Bundle 'matrix.vim' Bundle 'mru.vim' Bundle 'The-NERD-Commenter' "Bundle 'project.vim' Bundle 'restart.vim' Bundle 'taglist.vim' "Bundle 'templates.vim' "Bundle 'vimim.vim' Bundle 'ZenCoding.vim' Bundle 'css_color.vim' Bundle 'hallettj/jslint.vim'
3.2VIM使用
浏览内核源代码
为了实现类似SourceInsight功能,通过VIM+Ctags+Cscope+Taglist+Source Explore +NERD Tree实现.
安装插件
1)安装Ctags 和Cscope
Ubuntu下可以直接通过命令安装:
sudo apt-get install ctags cscope
注:cscope与ctags功能类似,但能补充Ctags的不足,二者结合便能兼具ctags的便利,双能用cscope补充ctags的局限,算得上是Linux内核代码分析的强大工具。
2)安装taglist :
通过向.vimrc中添加:
Bundle 'taglist.vim' Bundle "scrooloose/nerdtree" Bundle "wesleyche/SrcExpl" Bundle "majutsushi/tagbar"
3)再执行:BundleInstall命令即可自动安装
配置和使用插件
在Linux内核已经自带ctags和cscope的标签数据库的生成脚本,在内核源代码目录下通过命令可生成相应的数据库文件:
make ctags make cscope
注:前者,生成一个92M的tags文件,后者会生成cscope.files(分析文件的目录)和其它文件数据库.
配置ctags和cscope的数据库与VIM联动:
""" ctags database path """"""" set tags=/home/magc/workspace/linux-2.6.32.67/tags """ cscope database path """"""" set csprg=/usr/bin/cscope "whereis cscope set csto=0 "cscope DB search first set cst "cscope db tag DB search set nocsverb "verbose off "cs db path cs add /home/magc/workspace/linux-2.6.32.67/cscope.out set csverb "verbos off
taglist插件配置:
""""""""tag list setting """""""" filetype on nmap <F7> :TlistToggle<CR> "F7 Key = Tag list Toggling let Tlist_Ctags_Cmd = "/usr/bin/ctags" " whereis ctags let Tlist_Inc_WinWidth = 0 "window width change off" let Tlist_Exit_OnlyWindow = 0 let Tlist_Auto_Open = 0 "VIM打开时 let Tlist_Use_Right_Window = 1
Source Explorer 插件配置:
""""""" Source Explorer Setting """""" nmap <F4> :SrcExplToggle<CR> "control+h进入左边的窗口 nmap <C-H> <C-W>h "control+j进入下边的窗口 nmap <C-J> <C-W>j "control+k进入上边的窗口 nmap <C-K> <C-W>k "control+l进入右边的窗口 nmap <C-L> <C-W>l let g:Srcexpl_winHeight = 8 " // Set 100 ms for refreshing the Source Explorer let g:SrcExpl_refreshTime = 100 " // Set "Enter" key to jump into the exact definition context let g:SrcExpl_jumpKey = "<ENTER>" " // Set "Space" key for back the definition context let g:SrcExpl_gobackKey = "<SPACE>" let g:SrcExpl_isUpdateTags = 0
NERD Tree插件配置:
""""" NERD Tree Setting """"""""" let NERDTreeWinPos="left" nnoremap <F2> :NERDTreeToggle<CR>
注:上面设置中包含的快捷键有:
- F7打开Taglist
- F4打开Source Explorer
- F2打开Nerd Tree
- control+h进入左边的窗口
- control+k进入上边的窗口
- control+j进入下边的窗口
- control+l进入右边的窗口
代码浏览方法
类似SourceInsight的用法,只不过这里有些窗口需要手动打开,通过F2,F4,F7打开如上图的,先在左侧的NERD文件树中找到你要查看的文件,中间大窗口是源码窗口,当光标在某变量或函数名上时,下面就会显示它的定义,右侧是当前文件的标签(变量,函数名等)注:这里标签的来源是cscope和ctags两个数据库。
四、GCC编译器集合
4.1简述
GCC(GNU Compiler Collection)是一套由自由软件基金会发布的编译器集合,可以编译多种编程语言的程序,如C、C++、Objective-C、Fortran、Ada等。GCC是自由软件的代表之一,许多自由操作系统都采用了GCC作为默认的编译器。
GCC具有以下特点:
- 跨平台支持:GCC可在多个操作系统上使用,如Linux、Unix、Windows等。
- 编译器集合:包含了多种编程语言的编译器,可以满足不同领域和需求的开发者。
- 开源免费:完全开源,并且提供免费下载和使用。
- 高度优化:能够对生成的代码进行高度优化,以达到更好的性能和效率。
- 丰富的选项:提供大量参数选项来控制编译过程中的行为,可以调整生成代码质量和性能表现。
(1)基本用法
在使用Gcc 编译器的时候,我们必须给出一系列必要的调用参数和文件名称。GCC 编译器的调用参数大约有100多个,其中多数参数我们可能根本就用不到,这里只介绍其中最基本、最常用的参数。
GCC最基本的用法是∶gcc [options] [filenames]
其中options就是 编译器所需要的参数,filenames给出相关的文件名称。
- -c,只 编译,不链接成为 可执行文件, 编译器只是由输入的.c等 源代码文件生成.o为后缀的目标文件,通常用于编译不包含主程序的 子程序文件。
- -o output_filename,确定输出文件的名称为output_filename,同时这个名称不能和源文件同名。如果不给出这个选项,gcc就给出预设的 可执行文件a.out。
- -g,产生符号调试工具(GNU的gdb)所必要的符号资讯,要想对 源代码进行调试,我们就必须加入这个选项。
- -O,对程序进行优化 编译、链接,采用这个选项,整个 源代码会在编译、链接过程中进行优化处理,这样产生的 可执行文件的执行效率可以提高,但是,编译、链接的速度就相应地要慢一些。
- -O2,比-O更好的优化 编译、链接,当然整个编译、链接过程会更慢。
- -Idirname,将dirname所指出的目录加入到程序头文件目录列表中,是在 预编译过程中使用的参数。
C程序中的头文件包含两种情况∶
- A)#include <myinc.h>
- B)#include “myinc.h”
其中,A类使用尖括号(< >),B类使用双引号(“ ”)。对于A类, 预处理程序cpp在系统预设包含 文件目录(如/usr/include)中搜寻相应的文件,而B类,预处理程序在 目标文件的文件夹内搜索相应文件。
(2)基本规则
gcc所遵循的部分约定规则:
- .c为后缀的文件,C语言 源代码文件;
- .a为后缀的文件,是由 目标文件构成的档案库文件;
- .C,.cc或.cxx 为后缀的文件,是C++ 源代码文件且必须要经过 预处理;
- .h为后缀的文件,是程序所包含的头文件;
- .i 为后缀的文件,是C 源代码文件且不应该对其执行 预处理;
- .ii为后缀的文件,是C++ 源代码文件且不应该对其执行 预处理;
- .m为后缀的文件,是Objective-C 源代码文件;
- .o为后缀的文件,是 编译后的 目标文件;
- .s为后缀的文件,是 汇编语言 源代码文件;
- .S为后缀的文件,是经过 预编译的 汇编语言 源代码文件。
(3)执行过程
虽然我们称Gcc是C语言的 编译器,但使用gcc由C语言 源代码文件生成 可执行文件的过程不仅仅是编译的过程,而是要经历四个相互关联的步骤∶ 预处理(也称 预编译,Preprocessing)、 编译(Compilation)、 汇编(Assembly)和 链接(Linking)。
命令gcc首先调用cpp进行 预处理,在预处理过程中,对 源代码文件中的文件包含(include)、 预编译语句(如 宏定义define等)进行分析。接着调用cc1进行 编译,这个阶段根据输入文件生成以.o为后缀的目标文件。 汇编过程是针对汇编语言的步骤,调用as进行工作,一般来讲,.S为后缀的汇编语言 源代码文件和汇编、.s为后缀的汇编语言文件经过 预编译和汇编之后都生成以.o为后缀的目标文件。当所有的 目标文件都生成之后,gcc就调用ld来完成最后的关键性工作,这个阶段就是连接。在连接阶段,所有的 目标文件被安排在可执行程序中的恰当的位置,同时,该程序所调用到的 库函数也从各自所在的档案库中连到合适的地方。
4.2GCC的组成部分
GCC 是由许多组件组成的。表 1 列出了 GCC 的各个部分,但它们也并不总是出现 的。有些部分是和语言相关的,所以如果没有安装某种特定语言,系统:中就不会出现相关的文件。
表1:GCC 安装的各个部分
表2列出的软件和 GCC 协同工作,目的是实现编译过程。有些是很基本的(例如 as 和 Id),而其他一些则是非常有用但不是严格需要的。尽管这些工具中的很多都是各种 UNIX 系统的本地工具,但还是能够通过 GNU 包 binutils 得到大多数工具。
表2:GCC 使用的软件工具
4.3GCC 、gcc和g++的区别
GCC:GNU Compiler Collection(GUN 编译器集合),它可以编译C、C++、JAV、Fortran、Pascal、Object-C、Ada等语言。
gcc是GCC中的GUN C Compiler(C 编译器)
g++是GCC中的GUN C++ Compiler(C++编译器)
就本质而言,gcc和g++并不是编译器,也不是编译器的集合,它们只是一种驱动器,根据参数中要编译的文件的类型,调用对应的GUN编译器而已,比如,用gcc编译一个c文件的话,会有以下几个步骤:
- Step1:Call a preprocessor, like cpp.
- Step2:Call an actual compiler, like cc or cc1.
- Step3:Call an assembler, like as.
- Step4:Call a linker, like ld
由于编译器是可以更换的,所以gcc不仅仅可以编译C文件
所以,更准确的说法是:gcc默认调用了C compiler,而g++默认调用了C++ compiler。
实际使用中我们更习惯使用 gcc 指令编译 C 语言程序,用 g++ 指令编译 C++ 代码。需要强调的一点是,这并不是 gcc 和 g++ 的区别,gcc 指令也可以用来编译 C++ 程序,同样 g++ 指令也可以用于编译 C 语言程序。
实际上,只要是 GCC 支持编译的程序代码,都可以使用 gcc 命令完成编译。可以这样理解,gcc 是 GCC 编译器的通用编译指令,因为根据程序文件的后缀名,gcc 指令可以自行判断出当前程序所用编程语言的类别,比如:
- xxx.c:默认以编译 C 语言程序的方式编译此文件;
- xxx.cpp:默认以编译 C++ 程序的方式编译此文件。
- xxx.m:默认以编译 Objective-C 程序的方式编译此文件;
- xxx.go:默认以编译 Go 语言程序的方式编译此文件;
当然,gcc 指令也为用户提供了“手动指定代表编译方式”的接口,即使用 -x 选项。例如,gcc -xc xxx 表示以编译 C 语言代码的方式编译 xxx 文件;而 gcc -xc++ xxx 则表示以编译 C++ 代码的方式编译 xxx 文件。
但如果使用 g++ 指令,则无论目标文件的后缀名是什么,该指令都一律按照编译 C++ 代码的方式编译该文件。也就是说,对于 .c 文件来说,gcc 指令以 C 语言代码对待,而 g++ 指令会以 C++ 代码对待。但对于 .cpp 文件来说,gcc 和 g++ 都会以 C++ 代码的方式编译。
有读者可能会认为,C++ 兼容 C 语言,因此对于 C 语言程序来说,使用 gcc 编译还是使用 g++ 编译,应该没有什么区别,事实并非如此。严格来说,C++ 标准和 C 语言标准的语法要求是有区别的。举个例子:
#include <stdio.h> int main() { const char * a = "abc"; printStr(a); return; } int printStr(const char* str) { printf(str); }
如上所示,这是一段不规范的 C 语言代码。如果我们使用 gcc 指令编译,如下所示:
可以看到,该指令的执行过程并没有发生任何错误。而同样的程序,如果我们使用 g++ 指令编译,可以看到,GCC 编译器发现了 3 处错误。显然,C++ 标准对代码书写规范的要求更加严格。
除此之外对于编译执行 C++ 程序,使用 gcc 和 g++ 也是有区别的。要知道,很多 C++ 程序都会调用某些标准库中现有的函数或者类对象,而单纯的 gcc 命令是无法自动链接这些标准库文件的。举个例子:
#include <iostream> #include <string> using namespace std; int main(){ string str ="C语言中文网"; cout << str << endl; return 0; }
这是一段很简单的 C++ 程序,其通过 <string> 头文件提供的 string 字符串类定义了一个字符串对象,随后使用 cout 输出流对象将其输出。对于这段 C++ 代码,如果我们使用 g++ 指令编译,如下所示:
可以看到,使用g++可以正常编译,但使用gcc无法正常编译。其根本原因就在于,该程序中使用了标准库 <iostream> 和<string> 提供的类对象,而 gcc 默认是无法找到它们的。如果想使用 gcc 指令来编译执行 C++ 程序,需要在使用 gcc 指令时,手动为其添加 -lstdc++ -shared-libgcc 选项,表示 gcc 在编译 C++ 程序时可以链接必要的 C++ 标准库。也就是说,我们可以这样编译 demo.cpp 文件:
显然通过gcc来编译书写起来更加麻烦,于 C 语言程序的编译,我们应该使用 gcc 指令,而编译 C++ 程序则推荐使用 g++ 指令,这就足够了。
4.4GCC自动识别扩展名
通过前面的学习我们知道,对于执行 C 或者 C++ 程序,需要借助 gcc(g++)指令来调用 GCC 编译器。并且对于以 .c 为扩展名的文件,GCC 会自动将其视为 C 源代码文件;而对于以 .cpp 为扩展名的文件,GCC 会自动将其视为 C++ 源代码文件。
除此之外,GCC 编译器还可以自动识别多种扩展名(如表 3 所示),即根据不同的扩展名确定该文件该怎样编译。
表3 GCC自动识别的常用扩展名
注意,表 1 仅罗列了 GCC 编译器可识别的与 C 和 C++ 语言相关的文件后缀名。除此之外,GCC 编译器还支持 Go、Objective-C,Objective-C ++,Fortran,Ada,D 和 BRIG(HSAIL)等编程语言的编译,关于这些编程语言可被识别的文件扩展名,感兴趣的读者可前往GCC官网查看。
有读者可能会问,如果当前文件的扩展名和表 1 不符,还能使用 GCC 编译器吗?答案是肯定的。只需要借助 -x 选项(小写)指明当前文件的类型即可。例如:一个C程序,存储在名为demo的文件中:
//存储在 demo 文件中 #include <stdio.h> int main(){ puts("GCC教程:http://c.biancheng.net/gcc/"); return 0; }
显然,这是一段完整的 C 语言程序,但由于其存储在无扩展名的 demo 文件中,如果直接使用 gcc 指令调用 GCC 编译器,则执行会报错:
可以看到,GCC 编译器无法识别 demo 这个文件。这种情况下,就必须使用 -x 选项手动为其指定文件的类型,例如:
通过为 gcc 指令添加 -xc 选项,表明当前 demo 为 C 语言程序文件,由此 GCC 编译器即可成功将其编译为 a.out 可执行文件。除了用 c 表明 C 语言程序文件外,-x 指令还是后跟 c-header(C语言头文件)、c++(C++源文件)、c++-header(C++程序头文件)等选项。
GCC -std编译标准
任何一门语言都在不停的维护和更新,以 C 语言为例,发展至今该编程语言已经迭代了诸多个版本,例如 C89(偶尔又称为 C90)、C94(C89 的修订版)、C99、C11、C17,以及当下正在开发的 C2X 新标准。甚至于在这些标准的基础上,GCC 编译器本身还对 C 语言的语法进行了扩展,先后产生了 GNU90、GNU99、GNU11 以及 GNU17 这 4 个版本。
C++ 语言的发展也历经了很多个版本,包括 C++98、C++03(C++98 的修订版)、C++11(有时又称为 C++0x)、C++14、C++17,以及即将要发布的 C++20 新标准。和 C 语言类似,GCC 编译器本身也对不同的 C++ 标准做了相应的扩展,比如 GNU++98、GNU++11、GNU++14、GNU++17。
不同版本的 GCC 编译器,默认使用的标准版本也不尽相同。以当前最新的 GCC 10.1.0 版本为例,默认情况下 GCC 编译器会以 GNU11 标准(C11 标准的扩展版)编译 C 语言程序,以 GNU++14 标准(C++14 标准的扩展版)编译 C++ 程序。
们可以手动控制 GCC 编译器使用哪个编译标准吗?答案是肯定的,对于编译 C、C++ 程序来说,借助 -std 选项即可手动控制 GCC 编译程序时所使用的编译标准。也就是说,当使用 gcc 指令编译 C 语言程序时,我们可以借助 -std 选项指定要使用的编译标准;同样,当使用 g++ 指令编译 C++ 程序时,也可以借助 -std 选项指定要使用的编译标准。注意,不同版本的 GCC 编译器,所支持使用的 C/C++ 编译标准也是不同的。
举个例子,如下是一个 C 语言源程序:
#include <stdio.h> int main(){ for(int i=0;i<10;i++){ printf("i=%d ",i); } }
如果我们想以 c99 的标准编译它,在确认当前所有 GCC 编译器版本支持 C99 标准的前提下,通过执行如下指令,即可完成编译:
但是,对于在 for 循环中声明变量 i 的做法,是违反 C89 标准的。也就是说,如果我们以 C89 的编译标准编译 main.c,GCC 编译器会报错:
在编写程序前必须明确要使用的编译标准,并清楚得知道该标准下什么可用,什么不可用。
五、GDB调试工具
GDB(GNU Debugger)是一种用于调试程序的工具,由自由软件基金会发布。它可以帮助程序员了解程序运行时的状态,包括变量值、堆栈信息等,并且能够让程序暂停在某个指定位置进行调试。GDB支持多种编程语言,如C、C++、Objective-C、Fortran等。
GDB主要特点:
- 交互式命令行界面:可以通过命令行输入来控制GDB进行调试操作。
- 多平台支持:可在多种操作系统上使用,如Linux、Unix和Windows等。
- 支持多线程:可以对多线程程序进行调试。
- 支持远程调试:可以通过网络连接到远程机器进行调试。
- 功能强大:支持断点、单步执行、查看内存和寄存器等功能,还可以输出各种统计信息以供分析。
5.1什么是GDB
gdb是GNU debugger的缩写,是编程调试工具。
- GDB官网: https://www.gnu.org/software/gdb/
- GDB适用的编程语言: Ada / C / C++ / objective-c / Pascal 等。
- GDB的工作方式: 本地调试和远程调试。
目前release的最新版本为8.0,GDB可以运行在Linux 和Windows 操作系统上。
精选文章推荐阅读:
- [1]牛客网论坛最具争议的Linux内核成神笔记,GitHub已下载量已过百万
- [2]牛客网论坛考研计算机组成原理笔记,GitHub已下载量已过百万
- [3]探索网络通信核心技术,手写TCP/IP用户态协议栈,让性能飙升起来!
- [4]万字总结简化跨平台编译利器CMake,从入门到项目实战演练!
- [5]程序员性能之道,从使用perf开始!
安装与启动GDB
- gdb -v 检查是否安装成功,未安装成功则安装(必须确保编译器已经安装,如 gcc) 。
- 启动 gdb
- gdb test_file.exe 来启动 gdb 调试, 即直接指定需要调试的可执行文件名
- 直接输入 gdb 启动,进入 gdb 之后采用命令 file test_file.exe 来指定文件名
- 如果目标执行文件要求出入参数(如 argv[] 接收参数),则可以通过三种方式指定参数:
- 在启动 gdb 时,gdb --args text_file.exe
- 在进入gdb 之后,运行 set args param_1
- 在 进入 gdb 调试以后,run param_1 或者 start para_1
gdb的功能
- 启动程序,可以按照用户自定义的要求随心所欲的运行程序。
- 可让被调试的程序在用户所指定的调试断点处停住(断点可以是条件表达式)。
- 当程序停住时,可以检查此时程序中所发生的事。比如,可以打印变量的值。
- 动态改变变量程序的执行环境。
gdb的使用
运行程序
run(r)运行程序,如果要加参数,则是run arg1 arg2 ...
查看源代码
list(l):查看最近十行源码 list fun:查看fun函数源代码 list file:fun:查看flie文件中的fun函数源代码
设置断点与观察断点
break 行号/fun设置断点。 break file:行号/fun设置断点。 break if<condition>:条件成立时程序停住。 info break(缩写:i b):查看断点。 watch expr:一旦expr值发生改变,程序停住。 delete n:删除断点。
单步调试
continue(c):运行至下一个断点。 step(s):单步跟踪,进入函数,类似于VC中的step in。 next(n):单步跟踪,不进入函数,类似于VC中的step out。 finish:运行程序,知道当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。 until:当厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序知道退出循环体。
查看运行时数据
print(p):查看运行时的变量以及表达式。 ptype:查看类型。 print array:打印数组所有元素。 print *array@len:查看动态内存。len是查看数组array的元素个数。 print x=5:改变运行时数据。
程序错误
- 编译错:编写程序的时候没有符合语言规范导致编译错误。比如:语法错误。
- 运行时错误:编译器检查不出这种错误,但在运行时候可能会导致程序崩溃。比如:内存地址非法访问。
- 逻辑错误:编译和运行都很顺利,但是程序没有干我们期望干的事情。
gdb调试段错误
什么是段错误?段错误是由于访问非法地址而产生的错误。
- 访问系统数据区,尤其是往系统保护的内存地址写数据。比如:访问地址为0的地址。
- 内存越界(数组越界,变量类型不一致等)访问到不属于当前程序的内存区域。
gdb调试段错误,可以直接运行程序,当程序运行崩溃后,gdb会打印运行的信息,比如:收到了SIGSEGV信号,然后可以使用bt
命令,打印栈回溯信息,然后根据程序发生错误的代码,修改程序。
core文件调试
core文件
在程序崩溃时,一般会生成一个文件叫core
文件。core文件记录的是程序崩溃时的内存映像,并加入调试信息,core文件生成过程叫做core dump(核心已转储)
。系统默认不会生成该文件。
设置生成core文件
ulimit -c
:查看core-dump状态。ulimit -c xxxx
:设置core文件的大小。ulimit -c unlimited
:core文件无限制大小。
gdb调试core文件
当设置完ulimit -c xxxx
后,再次运行程序发生段错误,此时就会生成一个core
文件,使用gdb core
调试core文件,使用bt
命令打印栈回溯信息。
【文章福利】小编推荐自己的Linux内核技术交流群:【 865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!