PA的附录
PA的附录
PA附录上了一些很棒的关于用到的各工具的简单入门,包括Linux及命令行、git、gdb,这里略做一点笔记
Linux入门
这里是jyy的Linux入门教程,在PA0中推荐,因为涉及了几个有趣的脚本程序,故在这里记录一下。
命令行
Linux命令行中命令的一般格式:
1 | 命令名称 参数1 参数2 参数3 ... |
很显然,参数是由空格(空白字符)而非其他分隔符隔开的。
再三强调的命令:man
,用于查看手册。
为什么
cd
命令没有manpage,即无法使用man
命令查看cd
命令的手册?
https://stackoverflow.com/questions/41147818/no-man-page-for-the-cd-command
Linux中基本上所有命令本质上都是程序,例如执运行。行ls
命令时,shell读取系统的环境变量$PATH
,自动从PATH
位置的目录中寻找该命令程序
而对于cd
,由于当前工作目录(Current Working Directory)是由环境变量PWD
控制的,此变量也对应pwd
命令,即“print working directory”。由于子进程不能修改父进程的环境变量。因此,假设cd
是位于/bin/cd
的一条程序,运行/bin/cd
命令时,它将只会改变/bin/cd
及其所运行的任何内容的PWD
,而不会修改shell的PWD
。也就是此程序作为shell的子进程,无法修改父进程shell自身的环境变量。
The environment variables of a parent process cannot be changed by a child process. So if your shell ran
/bin/cd
which changedPWD
it would only affect/bin/cd
and anything it ran. It would not change the shell’sPWD
.
既然如此,那就需要将cd
命令内置进shell(built-in),而不能作为/bin
中的一条程序。因此为了展示程序帮助手册而设计的man
自然没有cd
的页面
不过还是可以使用其他命令来了解cd
:
1 | type cd |
也可以获取cd
命令的帮助页面:
1 | help cd |
也可以使用man builtins
来获取手册,cd相关的内容如下:
1 | cd [-L|[-P [-e]] [-@]] [dir] |
统计代码行数
统计一个目录中(包含子目录)中的代码行数
1 | find . | grep '\.c$\|\.h$' | xargs wc -l |
find
操作可以搜索目录中的文件,故grep
命令的输入内容便是shell当前工作目录下的所有文件grep
命令可以进行输入信息的过滤,其功能描述为:”print lines matching a pattern”.也就是说可以使用正则表达式来进行文本匹配,显然后面的字符串就是正则表达式了,此式表示匹配所有以.c
或.h
为后缀的文件- 连接这几条命令的操作符
|
称为管道符号,其左右都是shell命令,作用是将左边进程的标准输出连接至右边进程的标准输入(这里体现了UNIX设计哲学,有关这个可见:) wc
命令可以统计文件所占用的总行数,参数-l
用于计算代码行数。xargs
能够将标准输入转换为参数,传送给第一个参数所指定的程序
统计磁盘使用情况
以下命令统计/usr/share
目录下各个目录所占用的磁盘空间:
1 | du -sc /usr/share/* | sort -nr |
du
是磁盘空间分析工具, du -sc
将目录的大小顺次输出到标准输出, 继而通过管道传送给sort
. sort
是数据排序工具, 其中的选项-n
表示按照数值进行排序, 而-r
则表示从大到小输出. sort
可以将这些参数连写在一起.
/usr/share
中的目录过多, 无法在一个屏幕内显示. 此时, 我们可以再使用一个命令: more
或less
.
1 | du -sc /usr/share/* | sort -nr | more |
more
工具使用空格翻页, 并可以用q
键在中途退出. less
工具则更为强大, 不仅可以向下翻页, 还可以向上翻页, 同样使用q
键退出.
重定向
输出重定向>
可以使用objdump
命令对编译好的程序进行反汇编,假设编译程序为myprog
,若想将程序执行结果保存到output
文件中,可以执行命令:
1 | objdump -d myprog > output |
>
是标准输出重定向符号, 可以将前一命令的输出重定向到文件output
中.
使用了输出重定向之后, 屏幕上就不会显示myprog
输出的任何信息. 如果你希望输出到文件的同时也输出到屏幕上,使用tee
命令
1 | objdump -d myprog | tee output |
输出重定向可以简化许多功能,如:
创建文件:
1 | empty # 创建一个名为empty的空文件 |
复制文件:
1 | cat old_file > new_file # 将文件old_file复制一份, 新文件名为new_file |
输入重定向<
一个程序需要输入大量数据时(例如OJ测试):
1 | ./myprog < data |
<
是标准输入重定向符号, 可以将前一命令的输入重定向到文件data
中.
下面这个例子是在运行myprog
的同时, 指定其从文件data
中读入数据, 并将其输出信息打印到屏幕和文件output
中. time
工具记录了这一过程所消耗的时间, 最后你会在屏幕上看到myprog
运行所需要的时间
1 | time ./myprog < data | tee output |
如果你只关心myprog
的运行时间, 你可以使用以下命令将myprog
的输出过滤掉:
1 | time ./myprog < data > /dev/null |
/dev/null
是一个特殊的文件, 任何试图输出到它的信息都会被丢弃
使用Makefile管理工程
Make
像是一个脚本工具,可以一键执行组织好的一串编译指令,用于编译含有多个源代码文件的大项目. The make
utility requires a file, Makefile
(or makefile
), which defines set of tasks to be executed.
1 | hello:hello.c |
Makefile
文件由若干规则组成,其一般格式为
1 | 目标文件名:依赖文件列表 |
以hello
规则为例,它生成的目标文件是hello
,依赖于文件hello.c
,通过执行命令gcc hello.c -o hello
来生成hello
文件.
clean
规则用于清除编译结果,且不依赖于任何文件. 这条规则用于规定make clean
时要执行的命令
然而当文件中刚好存在名为clean
的文件时,make clean
会得到”文件已经是最新版本”的提示. 因此可以使用.PHONY
来告知make
工具clean
是一个伪目标,即不是一个真正的文件名,而是一个操作指令。
git
git 更是重量级,不必多介绍,这里只是简单入门:
第一步自然是安装 git,在 Ubuntu 中
1 | sudo apt install git |
安装好后进行一些配置:
1 | git config --global user.name "Nocturne2282282" |
初始化
如果使用 git clone
命令下载项目代码,则已经包含git
记录,不需要额外进行初始化。如果在其他的项目目录中使用git
,需要进行初始化:
1 | git init |
查看日志信息(存档信息)
查看目前为止的所有存档
1 | git log |
查看与当前存档相比,哪些文件发生了变化
1 | git status |
存档
首先使用 git status
查看是否有新的文件或已修改的文件未被跟踪(trace,我更愿意理解为“留痕”),若有则使用git add filename
将其加入跟踪列表,如:
1 | git add file.c |
也可以一次添加所有未被跟踪的文件
1 | git add -A |
然而这样可能会跟踪一些不必要的次生文件,如编译产生的.o
文件以及可执行文件等。一般只需要跟踪源代码文件即可,这一点可以通过配置.gitignore
文件来进行筛选(此文件一般是隐藏的,可以通过ls -a
命令看到)
提交工程当前的状态:
1 | git commit |
执行命令后将弹出文本编辑器,第一行应该添加本次提交的注释,实际工作中会有规范的提交内容。保存并退出编辑器后,存档成功,可以使用git log
查看存档记录。
读档
实际上存档与读档并不是 git 工作流的规定名称,而是拿游戏来举例的。当你需要读档的时候,可以先git log
查看已有的存档,然后决定回到哪个存档点。每一个存档都有唯一的 hash code,在使用 git 进行回档时,通过指定对应存档的 hash code 来读取指定存档:
1 | git reset --hard b87c |
显然 hash code 并没有这么短,不过实际应用中只需要输入存档的 hash code 前缀即可
注意:如同游戏一样,当你回到存档位置时,在那之后的所有记录都将消失!
分支
还有一种非常方便的方法可以避免上面的问题,即 git 的分支功能。
查看所有分支以及当前所在分支:
1 | git branch |
其中的master
或main
是主分支,在 git 初始化使自动创建。
读档时使用命令
1 | git checkout b87c |
则会进入一个虚构的分支,在此分支中可以查看b87
存档的内容,也可以通过git checkout
切换到其他分支
如果要修改代码内容,则需要保存到新的分支,可以使用
1 | git checkout-B [branchname] |
保存到新的分支中
GDB
How to Debug Bash Scripts
只是看 jyy 推荐的文章中推荐先看这一篇……
https://linuxconfig.org/how-to-debug-bash-scripts
some useful methods of debugging Bash scripts:
- How to use traditional techniques
- How to use the xtrace option
- How to use other Bash options
- How to use trap
下面提到了一个有用的技巧:断言(assertion),在 PA0 和 PA1 也有出现断言错误需要解决:
There are techniques used in traditional programming environments that can be useful with complex Bash scripts, such as using assertions. These are basically a ways of explicitly asserting conditions or the state of things at a point in time. Assertions can pinpoint even the subtlest of bugs. They may be implemented as a short function that shows the timing, line number and such, or something like this:
1 $ echo "function_name(): value of \\$var is ${var}"
How to use Bash xtrace option
我们一般倾向于写逻辑精简、单文件的 shell 脚本,故可以使用一些内置的 debug 选项来进行查错。这里介绍最有用的一个:xtrace
选项。
1 | bash -x <scriptname> |
此选项会使 shell 在执行每条语句前展示其求值(evaluate)后的结果。对比另一个选项:-v
与之相对,是展示每一条语句求值之前的结果。
这两个选项可以同时使用:-xv
,前者展示了变量带入(也可以表述为展开 expand)表达式后的语句,而后者则展示了变量带入前的语句内容。
How to use other Bash options
所有的调试选项都是默认关闭的,但是当你使用set
命令开启后便会保持开启,直到再次显式地关闭。可以使用$-
展示所有开启的选项,即变量。
-u
选项在一些时候也很有用。一般在 shell 执行 bash 脚本时不会输出某些错误信息,比如试图echo一个没有赋值的变量时,shell 并不会打印任何错误信息,但是开启-u
后便会打印出错误。
例如写一个简单的小程序
1 |
|
直接用bash
运行脚本以及加上-u
选项的结果如下:
1 | ❯ bash ./example.sh |
虽然但是,当运行更为复杂的脚本时,添加这些选项并不会使调试更加简单,相反还会因为输出了更多内容而变得更加复杂。
那是否可以只对特定的代码块开启所需的选项,从而只输出我们想要的部分的错误信息呢?
当然是有的,可以通过set
以及-options
+options
包裹起代码块来实现:
1 |
|
输出如下:
1 | ❯ bash ./example.sh |
f
选项可以关闭通配符How to use trap to help debug
trap(陷阱)以及前面提到的 assertion 技巧都可以用于处理较为复杂的脚本。
1 |
|
这是一个在EXIT
时的陷阱,也就是当到达脚本末尾时会触发这条陷阱语句执行(echo
语句)(显然EXIT
不需要生成显式的exit
语句)
另外一个有用的陷阱是DEBUG
,他在每一条语句执行后都会被触发: This happens after every statement, so it can be used as a brute force way to show the values of variables at each step in the script execution.
1 |
|
执行结果如下:
1 | ❯ bash ./example.sh |
GDB debugging tutorial for beginners
https://linuxconfig.org/gdb-debugging-tutorial-for-beginners
- How to install and use the GDB utility from the command line in Bash
- How to do basic GDB debugging using the GDB console and prompt
- Learn more about the detailed output GDB produces
Setting up GDB and a test program
话不多说,样例程序:
1 | int actual_calc(int a, int b){ |
Executing out script and configuring core dumps
1 | gcc -ggdb test.c -o test.out |
The
-ggdb
option togcc
will ensure that our debugging session using GDB will be a friendly one; it adds GDB specific debugging information to thetest.out
binary. We name this output binary file using the-o
option togcc
, and as input we have our scripttest.c
.
这里我们关注报错信息中的(core dumped)
,如果无法定位core dumped
文件,执行脚本
1 | if ! grep -qi 'kernel.core_pattern' /etc/sysctl.conf; then |
Here we are first making sure there is no Linux Kernel core pattern (
kernel.core_pattern
) setting made yet in/etc/sysctl.conf
(the configuration file for setting system variables on Ubuntu and other operating systems), and – provided no existing core pattern was found – add a handy core file name pattern (core.%p.%u.%s.%e.%t
) to the same file.The
sysctl -p
command (to be executed as root, hence thesudo
) next ensures the file is immediately reloaded without requiring a reboot. For more information on the core pattern, you can see the Naming of core dump files section which can be accessed by using theman core
command.Finally, the
ulimit -c unlimited
command simply sets the core file size maximum tounlimited
for this session. This setting is not persistent across restarts.
1 | xiaoma@xiaoma-virtual-machine:~/Downloads/gdbtest$ sudo bash coredump.sh |
现在再次运行test.out
文件便会看到coredump
文件
1 | xiaoma@xiaoma-virtual-machine:~/Downloads/gdbtest$ ./test.out |
测试:
1 | file core.4414.1000.8.test.out.1707581294 |
可以看到,这是一个64位的核心文件(64-bit core file),可以从.8.
判断这是8号信号终止了程序。8号信号是SIGFPE
,是一个浮点异常。稍后GDB会展示这是一个算数异常。
Using GDB to analyze the core dump
使用GDB打开核心文件
1 | gdb ./test.out ./core.4414.1000.8.test.out.1707581294 |
注意到第一行的GDB命令,第一个参数是二进制文件,第二个参数是核心文件,即 binary and core.
第22行的信息可以看到,程序是被一个SIGFPE
信号终止的,这是一个算术异常(Arithmetic exception)。这是一个很有用的信息!类似 java 中的异常,Arithmetic exception提示我们程序存在数学算数错误,而不是代码错误。
接下来23行可以看到程序帧#0
(frame,类似procedure,这个术语的含义可以再CS61A中看到,这里不多做讲解(我也不太明白其实,所以接下来的帧都用英文原文frame代替)). 可以看到关于此frame的很多信息,比如 procedure 的名称actual_calc
,变量为a=13, b=0
,位于test.c
文件的第3行
查看test.c
文件的第3行,看到代码 c=a/b
,显然这就是错误所在. 这就完成了对文件的错误排查.
Backtracing
Command bt
which is a shorthand for backtracing maybe the most often used:
1 | (gdb) bt |
此命令给出了对程序当前状态的跟踪(trace of the current state, that is, procedure after procedure called,of the program). 实际上大概就是Python报错中经常能看到的Traceback
,用于回溯问题根源. 另外,第一轮列的#0
等编号都是对应frame的标号.
这样就可以分析程序发生了什么:
the program started, and main()
was automatically called. Next, main()
called calc()
(and we can confirm this in the source code above), and finally calc()
called actual_calc
and there things went wrong.
Frame inspection
we can inspect each frame, the matching source code (if it is available), and each variable step by step:
转入#2
frame
1 | (gdb) f 2 |
列出源代码
1 | (gdb) list |
打印变量
1 | (gdb) p a |
GOTCHA
If the source code is different then the code which the binary was compiled from, one can be easily misled; the output may show non-applicable / changed source.
GDB does not check if there is a source code revision match! It is thus of paramount importance that you use the exact same source code revision as the one from which your binary was compiled.
现在转入frame #1
1 | (gdb) f 1 |
现在我们位于第12行,calc
函数中.接下来打印变量:
1 | (gdb) p a |
c
到现在(line 12)仍无法被打印,显然在当前上下文(context)中c
还是未定义的变量.
最后来看frame #0
1 | (gdb) f 0 |
(我的c
打印出来值是0,而教程为22010,原因可以再下面的探讨中得出)
显然我们已经定义了c
,但是还没有给定初始值,即还没有被表达式c=a/b
求值,(而且实际上失败了). 故c
实际上还是未定义的(undefined).The resulting value was likely read from some address space to which the variable c
was assigned (and that memory space was not initialized/cleared yet).