分类 Linux 相关 下的文章

虚拟以太网卡 veth

这是一块真实的台式机网卡, 左边是一个插网线的接口, 右边是电路板, 电路板上哪些金色的地方就插入主板的接口. 于是外部流量就通过网线, 进入网卡, 然后流入计算机, 计算机内部数据则通过相反的方向流出到外部.
网卡.png

虚拟以太网卡, 就是一个通过软件模拟的网卡设备, 它能模拟网卡的功能把数据传出去, 接收进来的数据, 在操作系统看来, 它就是一块功能完备的网卡. 这个虚拟的网卡一头连着操作系统的kernel, 另外一头呢? 它有没有网线可以插拔, 所以它只能连向另外一个网卡, 所以虚拟出来的网卡都是成对出现的, 它们不能插网线, 所以只能2个网卡之间相互通信, 就好比2个机器之间通过一条直连的网线连在了一起. 就好比下图这样:
vethPair.png

但是这个 veth0 和 veth1 对必然在同一台机器上, 不可能跨域两台机器还能连着. 不过 veth0 和 veth1 可以属于同一台机器上的不同VM, container 或者不同的 netns.

下面演示如何创建虚拟网卡:

# 新建之前查看已有的
$ ip link show

$ sudo ip link add  name veth1 type veth #新建link

$ ip link show # 再次查看
1994: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether fe:99:a3:6e:f7:b0 brd ff:ff:ff:ff:ff:ff
1995: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 56:64:02:ac:de:26 brd ff:ff:ff:ff:ff:ff

可以看到新加出来的 veth0 和 veth1 这对. 我们给的veth 的名字是 veth1, 而系统默认给出了配对的名字是 veth0. 最前面的数字 1994 和 1995 是系统的一个索引值, 称之为 IDX. @后面的名字是成对的另外一边点名字. <> 里面是支持的一些功能, 后边是一些相关参数, 这些参数可以在新建时设置, 也可以使用默认值. 下面一行代表 link 类型: ether(以太网), link 层地址(MAC 地址) 和 广播地址.

当然, 我们可以新建的时候, 直接给出这一对的名字, 比如:

$ sudo ip link add  name veth2 type veth peer name veth3

$ ip link show
1996: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 12:39:5b:12:84:49 brd ff:ff:ff:ff:ff:ff
1997: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 8a:f0:21:23:20:52 brd ff:ff:ff:ff:ff:ff

因为它们是默认一对出现的, 如果一方下线(down) 则另外一方默认就下线了:

$ sudo ip link set veth3  down

$ ip link
1996: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 12:39:5b:12:84:49 brd ff:ff:ff:ff:ff:ff
1997: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 8a:f0:21:23:20:52 brd ff:ff:ff:ff:ff:ff

可以看到, 我们这里新建的 veth 都只有link 层地址, 没有 IP 地址, 我们可以给它们赋予IP 地址.

$ ip addr show #设置 IP 地址之前先查看一下

$ sudo ip address add 192.168.88.1/16 dev veth3
$ sudo ip address add 192.168.88.2/16 dev veth2

$ ip addr show #设置完再次查看 
1996: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 12:39:5b:12:84:49 brd ff:ff:ff:ff:ff:ff
    inet 192.168.88.1/16 scope global veth3
       valid_lft forever preferred_lft forever
1997: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 8a:f0:21:23:20:52 brd ff:ff:ff:ff:ff:ff
    inet 192.168.88.2/16 scope global veth2
       valid_lft forever preferred_lft forever

$ sudo ip link set veth3 up #up

通过ldd找到依赖的共享库

经常做docker image, 就会考虑到底用哪个base image 最好? 是用 scratch, 还是用 busybox? 还是用 alpine? 这不仅仅关乎image 的大小, 还关乎带了哪些软件? 是不是经常要打漏洞的patch 等. 在一次尝试中, 忘记这些最精简的 image 中可能连最基本的glibc 都没有, 直接复制可执行文件上去, 发现无法运行, 于是开始研究一下到底缺少了哪些运行时库, 于是开始了研究ldd.


一开始, 我们做了一个最简单打印 hello world 的 c 代码, hello.c 如下:

#include<stdio.h>

int main(int argc, char* argv[]) {
    printf("hello  %s\n", "world");
}

然后编译 并本地运行:

$ gcc -o hello hello.c
$ ./hello
hello  world

开始制作 docker image, Dockerfile 内容:

FROM scratch
COPY ./hello /
CMD ["/hello"]

制作 docker image 的命令:

$ docker build -t helloimage .
Sending build context to Docker daemon  25.09kB
Step 1/3 : FROM scratch
 --->
Step 2/3 : COPY ./hello /
 ---> d86014f25f94
Step 3/3 : CMD ["/hello"]
 ---> Running in ca6e160bbde4
Removing intermediate container ca6e160bbde4
 ---> 51874d5170b3
Successfully built 51874d5170b3
Successfully tagged helloimage:latest

然后运行这个新的image:

docker run --rm helloimage
exec /hello: no such file or directory

为啥我明明已经复制 hello 这个可执行文件到了跟目录, 为啥还说没这个文件? 于是用 dive 命令去查看image的文件结构:
dive.png
从这个图中, 也明显看到: 该文件就是明明在那里.

这时候突然想到, 也许是它的动态链接库文件不存在, 导致它无法加载, 最终报出这个错误. 那么看看它到底需要什么动态链接库吧:

$ docker run --rm helloimage ldd /hello
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "ldd": executable file not found in $PATH: unknown.

奥, 原来这个scratch 啥都没有, ldd 当然也不存在了. 只好本地看看:

$ ldd ./hello
    linux-vdso.so.1 (0x00007ffec9f73000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5b7a06b000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f5b7a276000)

本地 ldd 给出了, hello 其实需要3个动态链接库. 第一个 vdso 其实是一个虚拟的动态链接库, 它不 map 到任何磁盘文件, 主要用来给 clib 使用并加速系统性能的. 第二个 libc.so.6 是 c 的lib, 它mapping 到磁盘文件 /lib/x86_64-linux-gnu/libc.so.6. 第三个 ld-linux-x86-64.so.2 其实是链接器本身.

$ ls -lah /lib/x86_64-linux-gnu/libc.so.6
lrwxrwxrwx 1 root root 12 Apr  6  2022 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.31.so
$ /lib64/ld-linux-x86-64.so.2
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]

所以, 我们复制过去的 hello 文件缺少2个动态库文件, 导致它无法运行. 那么如何解决呢?
方案一: 静态编译:
先静态编译hello.c 到 staticHello

$ gcc --static -o staticHello hello.c
$ ls
Dockerfile  hello  hello.c  staticHello

然后修改 Dockerfile:

FROM scratch
COPY ./staticHello /
CMD ["/staticHello"]

然后build image, 并运行:

$ docker build -t helloimage .
$ docker run --rm helloimage
hello  world

完美运行.

方案二: 复制这些缺少的动态库文件过去:
首先复制需要的2个动态库到当前目录 (因为build 的context 是当前目录, 其它目录文件它看不到)

cp /lib/x86_64-linux-gnu/libc.so.6 ./
cp /lib64/ld-linux-x86-64.so.2 ./

然后修改 Dockerfile:

FROM scratch
COPY libc.so.6 /lib/x86_64-linux-gnu/
COPY ld-linux-x86-64.so.2 /lib64/
COPY ./hello /
CMD ["/hello"]

接着 build image 并且运行:

docker build -t helloimage .
Sending build context to Docker daemon  3.121MB
Step 1/5 : FROM scratch
 --->
Step 2/5 : COPY libc.so.6 /lib/x86_64-linux-gnu/
 ---> d72d728c639a
Step 3/5 : COPY ld-linux-x86-64.so.2 /lib64/
 ---> dafed808d94e
Step 4/5 : COPY ./hello /
 ---> d90b74b43ead
Step 5/5 : CMD ["/hello"]
 ---> Running in 27d2b82c8d52
Removing intermediate container 27d2b82c8d52
 ---> 1c419a965e87
Successfully built 1c419a965e87
Successfully tagged helloimage:latest
$ docker run --rm helloimage
hello  world

完美运行.

所以, 2种方案都可以解决这个问题.

关于ldd

回过头来, 我们关注一下 ldd. ldd(List Dynamic Dependencies) 显示依赖的共享对象. 比如 ls 命令依赖这些动态库:

$ ldd /bin/ls
    linux-vdso.so.1 (0x00007fff7834a000)
    libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fb672c18000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb672a26000)
    libpcre2-8.so.0 => /usr/lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fb672995000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fb67298f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb672c7c000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb67296c000)

ldd 是怎么工作的? ldd 调用标准的链接器 ld.so, 同时设置环境变量 LD_TRACE_LOADED_OBJECTS=1, 这样 ld 就会检查所依赖的所有动态库, 并且找到合适的库并且加载到内存. 这样 ldd 就能记录这些被加载到库以及它们在内存的地址. vdso 和 ld.so 是2个特殊的库.

其实 ldd 是一个shell 脚本, 我们能查看它的源文件:

$ which ldd
/usr/bin/ldd
$ file /usr/bin/ldd
/usr/bin/ldd: Bourne-Again shell script, ASCII text executable
$ less /usr/bin/ldd

另外, 可执行文件的依赖库可以通过 objdump 找到:

$ objdump -p hello | grep NEEDED

GDB

GNU Debugger (GDB) 是 Linux & Unix 上广泛使用的 debugger, 可以对 C, C++, Objective-C, go 等程序 debug.

GDB 可以 1) 对正在运行的程序通过 attach 的方式进行 debug, 2) 也可以通过 gdb 新运行一个程序进行 debug, 3) 还可以直接对 core dump 进行有线的 debug.

GDB 命令 cheatsheet

  1. https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf
  2. https://gist.github.com/rkubik/b96c23bd8ed58333de37f2b8cd052c30

GDB 内部实现

GDB 使用 ptrace 系统调用去观察和控制其它程序的运行. 断点是通过替换原程序的某地址的指令为特殊的指令(int03)来实现的, 执行断点程序产生SIGTRAP中断.
关于 ptrace 特定的操作, 参看: https://man7.org/linux/man-pages/man2/ptrace.2.html

VI 快捷键

虽然 vi/vim 编辑器有很多快捷键, 但是常用的并不多. 把我认为需要常用的记录在这:

命令模式

i – 光标处插入(进入插入模式)
a – 光标后插入(进入插入模式)
A – 行尾插入(进入插入模式)
o – 新建一行(进入插入模式)
u – 撤销前面的改动
U – 撤销当前行的所有改动
D – 删除当前行光标后所有字符
x – 删除当前光标处字符
R – 当前行从光标处开始替换
r – 仅替换当前光标处字符, 之后还是命令模式
s – 替换当前光标处字符并且进入插入模式
S – 删除当前行所有字符, 回到当前行行首, 进入插入模式
~ – 当前字符大小写替换
dd – 删除当前行(还是命令模式)
3dd – 删除3行
dw – 删除一个字符
4dw – 删除4个字符
Shift+zz 保存并关闭

插入模式

ESC – 退出插入模式

导航(各种跳)

行跳跃

l - 向右
h - 向左
j - 向下
k - 向上

0 - (零字符)行首
^ - (正则表达式行开始字符) 行首第一个非空字符
$ - (正则表达式行结束字符) 行尾

屏幕跳跃

H – 屏幕第一行
M – 屏幕中间行
L – 屏幕最后一行

单词跳跃

WORD – 非空字符隔开的.
word – 字母,数字,下划线组成的串.
例如:
192.168.1.1 – single WORD
192.168.1.1 – seven words.

e – go to the end of the current word.
E – go to the end of the current WORD.
b – go to the previous (before) word.
B – go to the previous (before) WORD.
w – go to the next word.
W – go to the next WORD.

段落跳跃

{ - 段落开始处
} - 段落结尾处

ubuntu 20.04.4 安装 eBPF bcc

按理讲, 装个 bcc 有啥可记录的? 官方都有详细的安装说明, 直接一步步来不就好了. 其实我一开始也是这么想的. 然而现实很残酷, 花了我至少30分钟.

环境

supra@suprabox:~$ cat /etc/issue
Ubuntu 20.04.4 LTS \n \l

supra@suprabox:~$ uname -a
Linux suprabox 5.4.0-117-generic #132-Ubuntu SMP Thu Jun 2 00:39:06 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

官方安装文档

链接: https://github.com/iovisor/bcc/blob/master/INSTALL.md
关于 kernel 的配置, 由于 Ubuntu 20.04.4 的 kernel 已经是5.4.0, 所以默认已经全配置了.
由于官方说使用 package binary 的2种方式的 package 已经 outdated. 所以选用 source 编译安装.
自己编译需要 LLVM, Clang, cmake, gcc 根据不同的 Ubuntu 版本有不同的安装包, 复制命令执行就好

到真正安装和编译 BCC 的部分的时候, 出问题了:

git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
cmake ..
make
sudo make install
cmake -DPYTHON_CMD=python3 .. # build python3 binding
pushd src/python/
make
sudo make install
popd

首先, git clone 在这个国家 clone 不下来, 于是设置代理:

git config --global http.proxy http://proxy.mycompany:80
//如果代理需要用户名密码:
git config --global http.proxy http://mydomain\\myusername:mypassword@myproxyserver:8080/

好的, clone 成功, 然后一步一步安装编译, 编译的时候, 有出错了, 错误消息大概是这样:

/tmp/bcc/src/cc/bpf_module.cc:108:46: error: no matching function for call to ‘llvm::object::SectionRef::getName() const’
       auto sec_name = section.get()->getName();

这个帖子一样的问题, 还给出了解决方案. 我采用的是使用 v0.24.0 版本. 所以只要切换到这个 tag 就好了:

git checkout v0.24.0

安装完成之后, 直接执行测试命令:

supra@suprabox:/usr/lib/python3/dist-packages/bcc$ sudo ~/bpf/bcc/examples/hello_world.py
Traceback (most recent call last):
  File "/home/supra/bpf/bcc/examples/hello_world.py", line 9, in <module>
    from bcc import BPF
ImportError: No module named bcc

看到之前编译的时候, 使用的是 Python3, 所以看了一些, 系统默认的 python 是2.7:

supra@suprabox:~$ $(which python) --version
Python 2.7.18

于是改用 python3, 就好了:

sudo python3  ~/bpf/bcc/examples/hello_world.py
[sudo] password for supra:
b'         splunkd-5799    [005] .... 47245.389935: 0: Hello, World!'
b'         splunkd-5799    [005] .... 47245.393749: 0: Hello, World!'

supra@suprabox:~$ sudo python3 ~/bpf/bcc/examples/tracing/tcpv4connect.py
PID    COMM         SADDR            DADDR            DPORT
158736 python3.7    127.0.0.1        127.0.0.1        8089
838    qualys-cloud 10.249.64.103    64.39.104.103    443
159247 curl         127.0.0.1        127.0.0.1        8089
158736 python3.7    127.0.0.1        127.0.0.1        8089
159329 curl         127.0.0.1        127.0.0.1        8089

至此, 安装成功.