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 changed PWD it would only affect /bin/cd and anything it ran. It would not change the shell’s PWD.

既然如此,那就需要将cd命令内置进shell(built-in),而不能作为/bin中的一条程序。因此为了展示程序帮助手册而设计的man自然没有cd的页面

不过还是可以使用其他命令来了解cd

1
2
$ type cd
cd is a shell builtin

也可以获取cd命令的帮助页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ help cd
cd: cd [-L|[-P [-e]] [-@]] [dir]
Change the shell working directory.

Change the current directory to DIR. The default DIR is the value of the
HOME shell variable.

The variable CDPATH defines the search path for the directory containing
DIR. Alternative directory names in CDPATH are separated by a colon (:).
A null directory name is the same as the current directory. If DIR begins
with a slash (/), then CDPATH is not used.

If the directory is not found, and the shell option `cdable_vars' is set,
the word is assumed to be a variable name. If that variable has a value,
its value is used for DIR.

Options:
-L force symbolic links to be followed: resolve symbolic links in
DIR after processing instances of `..'
-P use the physical directory structure without following symbolic
links: resolve symbolic links in DIR before processing instances
of `..'
-e if the -P option is supplied, and the current working directory
cannot be determined successfully, exit with a non-zero status
-@ on systems that support it, present a file with extended attributes
as a directory containing the file attributes

The default is to follow symbolic links, as if `-L' were specified.
`..' is processed by removing the immediately previous pathname component
back to a slash or the beginning of DIR.

Exit Status:
Returns 0 if the directory is changed, and if $PWD is set successfully when
-P is used; non-zero otherwise.

也可以使用man builtins来获取手册,cd相关的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cd [-L|[-P [-e]] [-@]] [dir]
Change the current directory to dir. if dir is not supplied,
the value of the HOME shell variable is the default. Any addi‐
tional arguments following dir are ignored. The variable CDPATH
defines the search path for the directory containing dir: each
directory name in CDPATH is searched for dir. Alternative
directory names in CDPATH are separated by a colon (:). A null
directory name in CDPATH is the same as the current directory,
i.e., ``.''. If dir begins with a slash (/), then CDPATH is not
used. The -P option causes cd to use the physical directory
structure by resolving symbolic links while traversing dir and
before processing instances of .. in dir (see also the -P option
to the set builtin command); the -L option forces symbolic links
to be followed by resolving the link after processing instances
of .. in dir. If .. appears in dir, it is processed by removing
the immediately previous pathname component from dir, back to a
slash or the beginning of dir. If the -e option is supplied
with -P, and the current working directory cannot be success‐
fully determined after a successful directory change, cd will
return an unsuccessful status. On systems that support it, the
-@ option presents the extended attributes associated with a
file as a directory. An argument of - is converted to $OLDPWD
before the directory change is attempted. If a non-empty direc‐
tory name from CDPATH is used, or if - is the first argument,
and the directory change is successful, the absolute pathname of
the new working directory is written to the standard output.
The return value is true if the directory was successfully
changed; false otherwise.

统计代码行数

统计一个目录中(包含子目录)中的代码行数

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中的目录过多, 无法在一个屏幕内显示. 此时, 我们可以再使用一个命令: moreless.

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
2
3
4
5
6
7
hello:hello.c
gcc hello.c -o hello # 注意开头的tab, 而不是空格

.PHONY: clean

clean:
rm hello # 注意开头的tab, 而不是空格

Makefile文件由若干规则组成,其一般格式为

1
2
目标文件名:依赖文件列表
用于生成目标文件的命令序列 # 注意开头的tab, 而不是空格

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
2
3
4
git config --global user.name "Nocturne2282282"
git config --global user.email "206088020@qq.com"
git config --global core.editor vim
git config --global color.ui true

初始化

如果使用 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

其中的mastermain是主分支,在 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
2
3
4
5
6
#!/bin/bash

for i in 1 2 3
do
echo $i $j
done

直接用bash运行脚本以及加上-u选项的结果如下:

1
2
3
4
5
6
❯ bash ./example.sh
1
2
3
❯ bash -u ./example.sh
./example.sh: line 5: j: unbound variable

虽然但是,当运行更为复杂的脚本时,添加这些选项并不会使调试更加简单,相反还会因为输出了更多内容而变得更加复杂。

那是否可以只对特定的代码块开启所需的选项,从而只输出我们想要的部分的错误信息呢?

当然是有的,可以通过set以及-options +options包裹起代码块来实现:

1
2
3
4
5
6
7
8
9
#!/bin/bash

echo "ignore fileglobbing option turned off"
ls *

echo "ignore file globbing option set"
set -f
ls *
set +f

输出如下:

1
2
3
4
5
❯ bash ./example.sh
ignore fileglobbing option turned off
example.sh runtest.sh
ignore file globbing option set
ls: *: No such file or directory

f选项可以关闭通配符How to use trap to help debug

trap(陷阱)以及前面提到的 assertion 技巧都可以用于处理较为复杂的脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

trap 'echo score is $score, status is $status' EXIT

if [ -z $1 ]; then
status="default"
else
status=$1
fi

score=0
if [ ${USER} = 'superman' ]; then
score=99
elif [ $# -gt 1 ]; then
score=$2
fi

这是一个在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

trap 'echo "line ${LINENO}: score is $score"' DEBUG

score=0

if [ "${USER}" = "mike" ]; then
let "score += 1"
fi

let "score += 1"

if [ "$1" = "7" ]; then
score=7
fi
exit 0

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
❯ bash ./example.sh
line 5: score is
line 7: score is 0
line 11: score is 0
line 13: score is 1
line 16: score is 1
❯ bash ./example.sh 7
line 5: score is
line 7: score is 0
line 11: score is 0
line 13: score is 1
line 14: score is 1
line 16: score is 7

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int actual_calc(int a, int b){
int c;
c=a/b;
return 0;
}

int calc(){
int a;
int b;
a=13;
b=0;
actual_calc(a, b);
return 0;
}

int main(){
calc();
return 0;
}

Executing out script and configuring core dumps

1
2
3
gcc -ggdb test.c -o test.out
./test.out
Floating point exception (core dumped)

The -ggdb option to gcc will ensure that our debugging session using GDB will be a friendly one; it adds GDB specific debugging information to the test.out binary. We name this output binary file using the -o option to gcc, and as input we have our script test.c.

这里我们关注报错信息中的(core dumped),如果无法定位core dumped文件,执行脚本

1
2
3
4
5
if ! grep -qi 'kernel.core_pattern' /etc/sysctl.conf; then
sudo sh -c 'echo "kernel.core_pattern=core.%p.%u.%s.%e.%t" >> /etc/sysctl.conf'
sudo sysctl -p
fi
ulimit -c unlimited

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 the sudo) 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 the man core command.

Finally, the ulimit -c unlimited command simply sets the core file size maximum to unlimited for this session. This setting is not persistent across restarts.

1
2
3
xiaoma@xiaoma-virtual-machine:~/Downloads/gdbtest$ sudo bash coredump.sh 
[sudo] password for xiaoma:
kernel.core_pattern = core.%p.%u.%s.%e.%t

现在再次运行test.out文件便会看到coredump文件

1
2
3
4
xiaoma@xiaoma-virtual-machine:~/Downloads/gdbtest$ ./test.out 
Floating point exception (core dumped)
xiaoma@xiaoma-virtual-machine:~/Downloads/gdbtest$ ls
core.4414.1000.8.test.out.1707581294 coredump.sh test.c test.out

测试:

1
2
$ file core.4414.1000.8.test.out.1707581294 
core.4414.1000.8.test.out.1707581294: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './test.out', real uid: 1000, effective uid: 1000, real gid: 1000, effective gid: 1000, execfn: './test.out', platform: 'x86_64'

可以看到,这是一个64位的核心文件(64-bit core file),可以从.8.判断这是8号信号终止了程序。8号信号是SIGFPE,是一个浮点异常。稍后GDB会展示这是一个算数异常。

Using GDB to analyze the core dump

使用GDB打开核心文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ gdb ./test.out ./core.4414.1000.8.test.out.1707581294 
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./test.out...
[New LWP 4414]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `./test.out'.
Program terminated with signal SIGFPE, Arithmetic exception.
#0 0x000055f8e608113b in actual_calc (a=13, b=0) at test.c:3
3 c=a/b;
(gdb)

注意到第一行的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
2
3
4
(gdb) bt
#0 0x000055f8e608113b in actual_calc (a=13, b=0) at test.c:3
#1 0x000055f8e6081171 in calc () at test.c:12
#2 0x000055f8e608118a in main () at test.c:17

此命令给出了对程序当前状态的跟踪(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
2
3
(gdb) f 2
#2 0x000055f8e608118a in main () at test.c:17
17 calc();

列出源代码

1
2
3
4
5
6
7
8
9
(gdb) list
12 actual_calc(a, b);
13 return 0;
14 }
15
16 int main(){
17 calc();
18 return 0;
19 }

打印变量

1
2
(gdb) p a
No symbol "a" in current context.

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
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) f 1
#1 0x000055f8e6081171 in calc () at test.c:12
12 actual_calc(a, b);
(gdb) list
7 int calc(){
8 int a;
9 int b;
10 a=13;
11 b=0;
12 actual_calc(a, b);
13 return 0;
14 }
15
16 int main(){

现在我们位于第12行,calc函数中.接下来打印变量:

1
2
3
4
5
6
7
8
(gdb) p a
$1 = 13
(gdb) p b
$2 = 0
(gdb) p c
No symbol "c" in current context.
(gdb) p a/b
Division by zero

c到现在(line 12)仍无法被打印,显然在当前上下文(context)中c还是未定义的变量.

最后来看frame #0

1
2
3
4
5
6
7
8
9
(gdb) f 0
#0 0x000055f8e608113b in actual_calc (a=13, b=0) at test.c:3
3 c=a/b;
(gdb) p a
$3 = 13
(gdb) p b
$4 = 0
(gdb) p c
$5 = 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).