4-3 Linux C++编译、链接与调试
1.编译与链接过程
在Linux系统使用gcc/g++编译C++程序,其过程可分为4个阶段:
1.1 预处理
在预处理阶段做三件事:、
- 1.删除注释;
- 2.处理源文件中的#ifdef、#include和#define预处理命令;
- 3.将包含的头文件展开;
可使用 gcc -E 生成预处理后的中间文件*.i。
例如,有如下代码:
#include <stdio.h> #define MYNAME "Evila" int main() { // print Hello Evila! printf("Hello %s! \n", MYNAME); return 0; }
使用gcc -E main.c -o main.i
生成预处理后的文件,打开生成的main.i,可以看到<stdio.h>头文件展开,编写的注释被清理掉,宏在调用出被替换。
1.2 编译过程
编译阶段继续处理预处理阶段的产物,即main.i文件,编译期主要进行语法分析和词法分析,将符号汇总到符号表后生成汇编文件。
使用gcc -S main.i -o main.s
生成汇编文件:
符号表是编译期产生的一个hash列表,包括了变量、函数以及调试等信息,可以通过nm命令查看符号表。词法分析时向符号表里注册符号,当有重复定义的时候会报错,因为符号表已经存在该符号。
1.3 汇编阶段
汇编阶段继续处理编译阶段的产物,即main.s,汇编器会将汇编语言翻译成二进制的机器指令,并生成一份目标文件。
使用gcc -c main.s -o main.o
生成二进制目标文件,在Linux系统下以ELF文件的形式存储。一个gcc指令会生成一个二进制目标文件。如果某个代码工程中包含多个.h+.cpp文件时,会使用多个gcc指令进行编译,也生成了多个二进制目标文件。
1.3.1 ELF文件
ELF文件(Executable Linkable Format)是一种文件存储格式,Linux下的目标文件和可执行文件都按照该格式进行存储。ELF文件以按段(section)的形式进行分段存储,其中代码编译后的指令放在代码段(code section),未初始化的全局变量和局部静态变量放到bss区,已初始化的全局变量和局部静态变量放到data区。ELF的文件头记录了整个文件的属性信息,每个字段的含义可参考ELF文档。
使用readelf -h main.o
查看本例生成的main.o文件,可得:
$ readelf -h main.o ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 // 程序的入口地址,这是没有链接的目标文件所以值是0x00 Start of program headers: 0 (bytes into file) // 程序头的起始位置(非可执行文件为0) Start of section headers: 328 (bytes into file) // 段表开始位置的首字节 Flags: 0x0 Size of this header: 64 (bytes) // 当前ELF文件头的大小,这里是64字节 Size of program headers: 0 (bytes) // 程序头大小(非可执行文件为0) Number of program headers: 0 Size of section headers: 64 (bytes) // 段表的长度(字节为单位) Number of section headers: 13 // 段表中项数,也就是有多少段 Section header string table index: 10
1.3.2 段表及段(section)
ELF文件由各种各样的段组成,段表中记录了各个段的信息,以数组形式存放。段表的起始位置,长度,个数分别由ELF文件头中的Start of section headers,Size of section headers,Number of section headers指出。
使用readelf -S main.o
查看段表的详细信息如下:
There are 13 section headers, starting at offset 0x148: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000022 0000000000000000 AX 0 0 4 [ 2] .rela.text RELA 0000000000000000 000005a8 0000000000000048 0000000000000018 11 1 8 [ 3] .data PROGBITS 0000000000000000 00000064 0000000000000000 0000000000000000 WA 0 0 4 [ 4] .bss NOBITS 0000000000000000 00000064 0000000000000000 0000000000000000 WA 0 0 4 [ 5] .rodata PROGBITS 0000000000000000 00000064 0000000000000011 0000000000000000 A 0 0 1 [ 6] .comment PROGBITS 0000000000000000 00000075 000000000000002d 0000000000000001 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 0000000000000000 000000a2 0000000000000000 0000000000000000 0 0 1 [ 8] .eh_frame PROGBITS 0000000000000000 000000a8 0000000000000038 0000000000000000 A 0 0 8 [ 9] .rela.eh_frame RELA 0000000000000000 000005f0 0000000000000018 0000000000000018 11 8 8 [10] .shstrtab STRTAB 0000000000000000 000000e0 0000000000000061 0000000000000000 0 0 1 [11] .symtab SYMTAB 0000000000000000 00000488 0000000000000108 0000000000000018 12 9 8 [12] .strtab STRTAB 0000000000000000 00000590 0000000000000014 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
main.o共有13个Section:
- .text段保存代码编译后的指令,使用
objdump -s -d main.o
查看.text段详细内容 - .data段保存已初始化的全局静态变量和局部静态变量,使用查看
objdump -x -s -d main.o
.data段内容。 - .rodata段存保存只读数据,包括const修饰的变量和字符串常量,使用查看
objdump -x -s -d main.o
.rodata段内容。 - .symtab段是符号表段,以数组结构保存符号信息(函数和变量的地址),例如(1)定义在目标文件中的全局符号,可以被其他目标文件引用;(2)在本目标文件中引用的全局符号,却不在本目标文件中定义,比如pritnf。使用
readelf -s main.o
查看目标文件的符号表。
1.4 链接阶段
在1.1节编写的例子中,只有一个main.c文件,目前我们已经生成了目标文件main.o。但是main.o可以直接执行吗?答案:不可以,因为在这个代码中并没有定义“printf”的函数实现,且在预处理中包含进去的“stdio.h”中也只有该函数的声明,而没有定义函数的实现。
那么是在哪里实现的“printf”函数的呢?
答:printf函数的实现被打包到libc.so.6库文件中,gcc会到系统默认的搜索路径“/usr/lib”下进行查找libc.so.6等库文件并执行链接。libc.so.6库避免了重复的开发printf等函数,使用时只需要include stdio.h
,并链接libc.so.6库到可执行文件即可。由于经过预处理、编译和汇编阶段生成的多个目标文件都是非可执行文件且是相互离散的,因此需要链接器将多个目标文件以及使用到的库文件进行合并,最终生成可执行文件。
链接器的主要工作可总结为以下4点:
(1)合并各个目标文件的段内容;
(2)调整合并后段的起始位置;
(3)合并符号表,并进行符号分析;
(4)进行符号重定位工作;
2.静态链接库
2.1 静态链接及其特点
静态链接是在链接期间将源文件使用到的库代码与编译生成的目标文件进行链接合并,生成的可执行文件。可执行文件是静态链接库与编译生成的目标文件集合,在运行时间与静态链接库再无关联。静态链接具有以下三个特点:
- 静态链接会增大空间和资源的消耗,因为所有相关的目标文件与函数库被链接合成一个可执行文件
- 如果静态库有升级或变动,可执行文件需要重新编译、链接
- 程序在运行时与函数库再无关联,运行时无关环境是否具有需要的库,方便移植
2.2 制作静态库(.a)
静态链接库通常以lib作为前缀、.a作为后缀。在Linux系统中使用ar命令将一组目标文件(.o)打包形成静态链接库,例如编写一个函数printName,它接受一个string参数,功能是输出这个string参数:
// comm_func.h #include <string> #include <iostream> using std::string; void printName(string name);
// comm_func.cpp #include "comm_func.h" void printName(string name) { std::cout<< " I am " << name << std::endl; }
可按照如下步骤将其制作成静态链接库:
- 使用
gcc -c comm_func.cpp -o comm_func.o
命令将其编译为可重定向目标文件。注意,必须加-c,否则直接编译为可执行文件。
- 使用
- 使用
ar -rcs libcomm.a comm_func.o
命令将comm_func.o制作成静态库,静态库的命名规则是lib开头,.a结尾。
- 使用
2.3 使用静态库
经过上述两个步骤静态链接库已经制作成功,此时的项目目录如下所示:
tree . ├── include │ └── comm_func.h ├── obj │ ├── comm_func.o │ └── libcomm.a └── source ├── comm_func.cpp └── main.cpp 3 directories, 5 files
为了使用刚刚生成的静态链接库,在之前编写的main.cpp中调用这个库函数:
// main.cpp #include <string> using std::string; #include "comm_func.h" // 必须要包含头文件 int main() { printName("Evila"); // 调用静态库中的函数 return 0; }
在编译main.cpp时,由于要使用静态链接库,需要指定以下四个参数:
- 1.指定静态链接库的搜索路径,-L参数
- 2.指定静态库名,不需要lib前缀和.a后缀,-l参数
- 3.指定头文件搜索路径, -I参数
- 4.指定静态链接的方式, -static
使用如下命令来对main.cpp进行编译和链接:g++ -static main.cpp -I../include -L../obj -lcomm -o main
,生成了可执行文件main。
可以发现,可执行文件main的文件大小达到了1.4M,运行main发现程序正常执行符合预期。
2.4 静态链接解析步骤
g++ -static main.cpp -I../include -L../obj -lcomm -o main
在这个过程中,使用到静态库libcomm.a。由于main.cpp中使用了libcomm.a中的printName函数,因此链接时,会将libcomm.a中引用的代码“拷贝”到最终的可执行文件main中。
特别注意,必须把-lcomm放在main.cpp后面,即静态库需要放在引用它的位置的右边。因为静态库在链接时的解析过程如下:
1、链接器从左往右扫描可重定位目标文件和静态库
2、扫描main.cpp时,发现一个未解析的符号printName,记住这个未解析的符号
3、扫描libcomm.a,找到了前面未解析的符号,提取相关代码
4、最终没有任何未解析的符号,编译链接完成
由于最终生成的可执行文件中已经包含了printName相关的二进制代码,因此这个可执行文件在一个没有libcomm.a的linux系统中也能正常运行。
3.动态链接库
由于静态链接时将源文件引用的静态库打包进了可执行文件,在可执行文件启动时内存中就会存在引用到的静态库。并且,当不同的可执行文件静态链接了同一份静态库时,在内存中就会有同一个静态库的多个拷贝副本。
例如,静态库comm.a被链接到两个可执行文件中,当两个可执行文件加载到内存中时,内存中存在了comm.a库的多个拷贝副本。这对于内存资源来说无疑是一种浪费,能否让库文件在内存中只有一个实例,并供给多个引用它的可执行文件共享?
3.1 动态链接与动态链接库
动态链接并不在链接时将需要的二进制代码都“拷贝”到可执行文件中,而是仅仅“拷贝”一些重定位和符号表信息,这些信息可以在程序运行时完成动态链接库的内容映射到运行时相应进程的地址空间。linux中通常以.so(shared object)作为后缀,使用-fPIC -shared制作动态链接库,-fPIC -shared表示生成位置无关代码,以便在只有一个副本的情况下供多个应用程序共享。
动态库的名字必须与lib开头,以.so结尾。同样的,以制作静态库时的comm_func.h和comm_func.cpp源代码为例,使用如下命令将其制作成动态链接库:
若代码的目录按如下结构组织:
tree ├── include │ └── comm_func.h ├── obj │ ├── comm_func.o │ ├── libcomm.a └── source ├── comm_func.cpp └── main.cpp 3 directories, 6 files
- 使用
g++ comm_func.cpp -I../include -fPIC -shared -o ../obj/libcomm.so
命令将comm_func.o制作成动态链接库。
3.2 使用动态链接库
为了使用刚刚生成的动态链接库,同样的在之前编写的main.cpp中调用这个库函数:
// main.cpp #include <string> using std::string; #include "comm_func.h" // 必须要包含头文件 int main() { printName("Evila"); // 调用静态库中的函数 return 0; }
在编译main.cpp时,由于要使用动态链接库,需要指定以下四个参数:
- 1.指定动态链接库的搜索路径,-L参数
- 2.指定动态库名,不需要lib前缀和.a后缀,-l参数
- 3.指定头文件搜索路径, -I参数
使用如下命令来对main.cpp进行编译和链接:g++ main.cpp -I../include -L../obj -lcomm -o main
,生成了可执行文件main。
可以发现,可执行文件main的文件大小仅8.8K。
3.3 动态链接库加载
接上节,此时已经编译链接生成了可执行文件main。在Linux下执行main,得到找不到共享库的错误:
使用ldd命令也可以发现可执行文件动态库链接存在问题:
原因是:运行机的操作系统还未加载libcomm.so这个动态链接库,此时需要利用ldconfig命令将动态链接库为系统所共享。
ldconfig命令的用途, 主要是在默认搜寻目录(/lib和/usr/lib)以及动态库配置文件/etc/ld.so.conf内所列的目录下, 搜索出可共享的动态链接库(格式如lib.so), 进而创建出动态装入程序(ld.so)所需的连接和缓存文件。缓存文件默认为/etc/ld.so.cache, 此文件保存已排好序的动态链接库名字列表。
因此,需要在以下三个选项中选择一个将libcomm.so加载:
- 将共享库文件移动到/lib或/usr/lib目录下,执行ldconfig命令。
- 如果共享库文件安装到了/usr/local/lib或其它"非/lib或/usr/lib"目录下, 把共享库所在目录加入到共享库配置文件/etc/ld.so.conf中,执行ldconfig命令。
- 如果共享库文件安装到了其它"非/lib或/usr/lib" 目录下, 但是又不想在/etc/ld.so.conf中加路径(或者是没有权限加路径)。export一个全局变量LD_LIBRARY_PATH, LD_LIBRARY_PATH指定了loader在哪些目录中可以找到共享库,然后运行程序的时候就会去这个目录中找共享库。例如,在我的开发机上,libcomm.so所在的目录为
/home/hemsworthli/compile/obj+libcomm.so
,则可以在.bashrc或.bash_profile或shell里加入以下语句即可:export LD_LIBRARY_PATH=/home/hemsworthli/compile/obj:$LD_LIBRARY_PATH
。
- 如果共享库文件安装到了其它"非/lib或/usr/lib" 目录下, 但是又不想在/etc/ld.so.conf中加路径(或者是没有权限加路径)。export一个全局变量LD_LIBRARY_PATH, LD_LIBRARY_PATH指定了loader在哪些目录中可以找到共享库,然后运行程序的时候就会去这个目录中找共享库。例如,在我的开发机上,libcomm.so所在的目录为
经过ldconfig后,再次使用ldd命令查看可执行文件的链接情况,发现没有链接失败的符号出现,执行main结果正常。
4.GDB调试指南
GDB(GNU Debugger)是Linux下的调试⼯具,可以调试c、c++、objective-c、go、java、pascal等语⾔。本节以C程序为例,介绍GDB启动调试的步骤和⽅式,具体内容分为以下几部分:
- 1.介绍哪些程序可以被GDB工具调试
- 2.GDB调试可执行文件的命令
- 3.GDB调试core文件的命令
- 4.设置断点命令
- 5.断点中断处查看变量
- 6.单步调试命令
4.1 可被GDB调试的程序
对于C/C++程序来说,需要在编译时加上-g参数,目的是保留调试信息,否则不能使⽤GDB进⾏调试。
如果不是不知道可执行文件是否在编译时携带有-g参数,如何判断⼀个⽂件是否带有调试信息呢?例如:
使用如下代码进行测试:
#include <stdio.h> int main() { printf("Hello Evila!"); return 0; }
$ gdb main Reading symbols from /data/home_dir/hemsworthli/compile/source/main...(no debugging symbols found)...done.
如果没有调试信息,会提示no debugging symbols found
,如果有调试信息,会提示Reading symbols from main...done.
4.2 gdb调试可执行文件
经过g++ main.cpp -o main -g
编译出可执行文件main后,直接使用gdb + main命令进入gdb调试控制台:
$ gdb main GNU gdb (GDB) Red Hat Enterprise Linux (7.2-50.el6) ... ... Reading symbols from /data/home_dir/hemsworthli/compile/source/main...done. (gdb)
再输入run,即程序开始运行。
$ (gdb) run Starting program: /data/home_dir/hemsworthli/compile/source/main Hello Evila! Program exited normally.
若程序需要输入参数,可在run命令后跟着输入的参数;也可以在run命令之前使用set args命令设置启动参数.
4.3 gdb调试core文件
当程序core dump时,可能会产⽣core⽂件,core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,调试core文件是帮助定位程序问题的杀手锏。但前提是系统没有限制core⽂件的产⽣。
可以使⽤命令limit -c查看:
$ ulimit -c 0
如果结果是0,即使程序core dump了也不会有core⽂件留下。
需要让core⽂件能够产⽣,需要使用下面两个命令:
ulimit -c unlimited #表示不限制core⽂件⼤⼩ ulimit -c 10 #设置最⼤⼤⼩,单位为块,⼀块默认为512字节
若有如下程序:
#include <stdio.h> int main() { char *str = "hello Evila!"; str[0] = 0; return 0; } 编译完成后执行,发生段错误core dumped $ ./main Segmentation fault (core dumped) $ ls -a 有core文件产生 . .. core.main main main.cpp
调试core⽂件也很简单,直接使用gdb+core.main命令进行gdb控制台:
gdb core.main GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-80.el7 Copyright (C) 2013 Free Software Foundation, Inc. ... ... Core was generated by `./main'. Program terminated with signal 11, Segmentation fault.
接着使用bt命令查看堆栈信息,即可定位到core dumped所在的位置。
4.4 设置断点
在使用GDB调试时,可以在指定位置设置断点,程序运⾏到该断点位置将会“暂停”,这个时候我们就可以对程序进⾏更多的操作,⽐如查看变量内容,堆栈情况等等,以帮助我们调试程序。
例如,有如下代码:
#include<stdio.h> void printNum(int a) { printf("printNum:%d\n", a); } void printStr(char* pStr) { printf("printStr: %s\n", pStr); } int main(int argc,char *argv[]) { int a = 0; printNum(a); printStr("Hello Evila"); return 0; }
在编译时携带-g参数生成可执行文件:g++ main.cpp -o main -g
,使用gdb调试可执行文件main:
- 根据行号设置断点:
b main.cpp 14
,该命令在main.cpp的第14行设置了断点 - 根据函数名设置断点:
b printNum
, 该命令在printNum函数入口处设置断点 - 根据字符匹配规则设置断点:
rbreak print*
,该命令会将所有以print为前缀的函数入口处设置断点 - 根据条件设置断点:
b main.cpp:14 if a == 0
,当a=0时,程序会在14行处停住 - 根据表达式值变化产⽣断点:
watch a
,让程序运⾏时,如果a的值发⽣变化则会停住 - 使用
info breakpoints
查看已设置的断点(gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x00000000004005af in printNum(int) at main.cpp:4 2 breakpoint keep y 0x00000000004005d1 in printStr(char*) at main.cpp:9 3 breakpoint keep y 0x0000000000400488 <printf@plt> 4 breakpoint keep y 0x0000000000400488 <printf@plt> (gdb)
可以发现 每个断点都有自己的编号,便于删除和禁用/启用断点: - 启用和禁用断点:
disable #禁⽤所有断点 disable bnum #禁⽤标号为bnum的断点 enable #启⽤所有断点 enable bnum #启⽤标号为bnum的断点 enable delete bnum #启动标号为bnum的断点,并且在此之后删除该断点
- 清除断点:
clear #删除当前⾏所有breakpoints clear function #删除函数名为function处的断点 clear filename:function #删除⽂件filename中函数function处的断点 clear lineNum #删除⾏号为lineNum处的断点 clear f:lename:lineNum #删除⽂件filename中⾏号为lineNum处的断点 delete #删除所有breakpoints,watchpoints和catchpoints delete bnum #删除断点号为bnum的断点
4.5 查看变量
在启动调试以及设置断点之后,就到了非常关键的一步-查看变量。GDB调试最大的目的之一就是走查代码,查看运行结果是否符合预期。既然如此,我们就不得不了解一些查看各种类型变量的方法,以帮助我们进一步定位问题。
同样的,使用如下程序讲述查看变量的方法:
#include<stdio.h> #include<stdlib.h> int main() { int a = 10; // 整型 int i = 0; int b[] = {1,2,3,5}; // 数组 char c[] = "Hello Evila"; // 字符数组 /*申请内存,失败时退出*/ int *d = (int*)malloc(a*sizeof(int)); if(NULL == d) { printf("malloc error\n"); return -1; } /*赋值*/ for(i=0; i < 10;i++) { d[i] = i; } free(d); d = NULL; float e = 8.5f; return 0; }
编译:
g++ main.cpp -o main -g
使用gdb调试main,在24行设置断点:
(gdb) b 24 Breakpoint 1 at 0x4006ee: file main.cpp, line 24.
使用run命令启动程序:
(gdb) run Starting program: /data/home_dir/hemsworthli/compile/source/main Breakpoint 1, main () at main.cpp:24 24 return 0;
此时程序启动,停在设置的断点24行处,此时我们可以查看变量值:
查看基本类型变量,数组,字符数组,使用print(可简写为p)打印变量内容:
(gdb) p a $1 = 10 (gdb) p b $2 = {1, 2, 3, 5} (gdb) p c $3 = "Hello Evila" (gdb)
查看指针变量内容:
(gdb) p d $4 = (int *) 0x0 (gdb)
因为程序运行到了24行,此时指针d已经被赋值成了NULL,若我们在21行也设置一个断点并重新运行:
(gdb) p d $1 = (int *) 0x601010 (gdb)
gdb支持指针解引用,即查看指针指向的变量值:
(gdb) p *d $2 = 0 (gdb)
如果要查看数据的多个值,需要在数组名的后面跟上@并加上要打印的长度,或者@后面跟上变量值:
(gdb) p *d@10 $4 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} (gdb) p *d@a // 这里 a=10 $5 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} (gdb)
4.6 按照特定格式打印变量
对于简单的数据,print默认的打印方式已经足够了,它会根据变量类型的格式打印出来。但是有时候这还不够,我们需要查看变量在不同格式下的值。常见格式控制字符如下:
- x 按十六进制格式显示变量。
- d 按十进制格式显示变量。
- u 按十六进制格式显示无符号整型。
- o 按八进制格式显示变量。
- t 按二进制格式显示变量。
- a 按十六进制格式显示变量。
- c 按字符格式显示变量。
- f 按浮点数格式显示变量。
例如, 使用命令p c
查看了字符串c的值:
(gdb) p c $7 = "Hello Evila" (gdb)
但是如果我们要查看它的十六进制格式打印呢?
(gdb) p/x c $8 = {0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x45, 0x76, 0x69, 0x6c, 0x61, 0x0} (gdb)
二进制:
(gdb) p/t c $11 = {1001000, 1100101, 1101100, 1101100, 1101111, 100000, 1000101, 1110110, 1101001, 1101100, 1100001, 0} (gdb)
4.7 单步调试命令
至此,已经了解了GDB调试启动,设置断点,查看变量的方法,而设置断点往往需要配合单步调试进行,以达到程序是指在我们的控制之下,按要求执行语句。
- 单步执行-next
next命令(可简写为n)用于在程序断住后,继续执行下一条语句,如果后面跟上数字n,则表示执行该命令n次,就达到继续执行n行的效果了。 - 单步进入-step
如果我们想跟踪函数内部的情况,可以使用step命令(可简写为s),它可以单步跟踪到函数内部,但前提是该函数有调试信息并且有源码信息。 - 继续执行到下一个断点-continue
我们可能打了多处断点,或者断点打在循环内,这个时候,想跳过这个断点,甚至跳过多次断点继续执行该怎么做呢?可以使用continue命令(可简写为c)或者fg,它会继续执行程序,直到再次遇到断点处。 - 继续运行到指定位置-until
可以使用until命令 + 行号(可简写为u)控制程序运行到制定代码行。 - 查看源码:list命令(可简写为l),l后面可以跟行号,表明要列出附近的源码;l后面跟函数名,表明列出某个函数的源码;set listsize命令支持修改list命令展示源码的行数;list支持列出指定行之间的源码,也支持列出某个文件的源码