ICS 期末复习

Last updated on December 29, 2024 12:38 AM

Chap. 7 Linking

关于编译器驱动程序:

  • cpp [other args] main.c /tmp/main.i 是预处理器,main.i 是 ASCII 中间文件。
  • cc1 /tmp.main.i -Og [other args] -o /tmp/main.s 是编译器,main.s 是 ASCII 汇编代码。
  • as [other args] -o /tmp/main.o /tmp/main.s 是汇编器,main.o可重定位目标文件
  • ld 是链接器。

ELF 的各个节:

  • 0:ELF header
  • 11:节头部表
  • 4:.bss目标文件中不占空间,在运行时分配。
  • 5:.symtab,不一定要 -g 才能得到。
  • .debug.line 才是需要 -g 的。前者存局部变量

有三个伪节(在可执行文件中不存在)

  • COMMON未初始化的全局变量
  • UNDEF:未定义的外部符号
  • ABS:不该被重定位的符号

有多个弱符号重名的时候会随便选一个。


静态库编译:每个函数弄一个 .o,然后打包成 .a。用 ar 打包。

链接器解析引用的流程:

  • 定义可重定位目标文件的集合 EE,未解析符号 UU,已定义符号 DD
  • 对于每个文件,若是 .o,正常进行维护
  • 若是 .a,尝试寻找包含 UU 中符号定义的 .o 成员,找到了就加进去 EE,然后相应更新 UUDD

.o 的顺序无冇所谓,但是 .a 的需要注意。

把依赖关系图画出来,按着有向边的方向走一遍。


重定位的步骤:

  • 重定位节与符号定义:把所有 .o 的同类型节 merge 到一起,然后把运行时内存地址赋给节、符号。
  • 重定位节的符号引用:字面意思

代码的重定位条目在 .rel.text已初始化数据的重定位条目在 .rel.data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
foreach section s {
foreach relocation entry r {
refptr = s + r.offset
// 此时已经知道了节的地址 ADDR(s) 和对象的地址 ADDR(r.symbol)
// refptr 是我们要修改的地址(在这个地址引用了符号)
if (r.type == R_X86_64_PC32) {
// PC相对寻址
refaddr = ADDR(s) + r.offset // 这是 ref 的运行时地址
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr)
// r.addend = 4
// r.addend 实际上等于 %rip - refaddr,%rip 就是下一条指令的地址
} else if (r.type == R_X86_64_32) {
*refptr = (unsigned) (ADDR(r.symbol) + r.addend)
// 这种情况 r.addend = 0,所以其实就是直接把地址弄上去
}
}
}

可执行目标文件多了一个 .init 节,里面有 _init 函数。由于不需要重定位所以没有 .rel 了。

代码段总从 0x400000 开始。用户栈从 24812^{48}-1 开始往下增长。

注意到由于 ASLR(地址空间布局随机化)的存在,绝对地址会变,相对的不变。

加载器过程:_start->__libc_start_main->main


动态链接:共享目标文件 .so,里面有个 interp 节,包含了动态链接器的路径名,动态链接器 ld.so 本身也是个共享目标文件


TODO:PIC, GOT, PLT

Chap. 8 ECF

异常表,异常表基址寄存器 + 84*异常号

异常处理程序运行在内核态

四种异常:

  • 中断 interrupt:异步,总是返回到下一条。e. g. I/O
  • 陷阱 trap:同步,有意的异常,系统调用
  • 故障 fault:可能返回到当前,常见的缺页,潜在可恢复
  • 终止 abort:不会返回,致命错误,硬件的那种

系统调用:

%rax:系统调用号以及返回值。出现故障时等于负的 errno

参数的顺序与过程调用不同:%rdi,%rsi,%rdx,%r10,%r8,%r9。寄存器 %rcx 和 %r11 会被破坏


进程上下文:内存中的代码与数据,栈,寄存器,PC,环境变量,opened fd

每个进程有自己的私有地址空间,代码段从 0x400000 开始。顶部空间给内核。

内核模式:控制寄存器中 mode bit,可以停止处理器,改变模式位,发起 IO 操作,访问任何地址。

用户模式引用内核区的地址会导致保护故障


四种导致进程停止的信号:

  • SIGSTOP
  • SIGTSTP:terminal stop
  • SIGTTIN:TTY input for bg
  • SIGTTOU:TTY output for bg

后两者的意思是:后台进程想要通过终端进行 I/O 操作的话必须要停下来直到转为前台。


waitpid 的 options:

  • 默认:挂起,等待集合中任意子进程终止
  • WNOHANG:不挂起,如果没有的话,立即返回 0
  • WUNTRACED:多关心停止的进程
  • WCONTINUED:多关心收到 SIGCONT 然后继续的进程

可以用按位或给他们或起来。

wait 不等子进程的子进程


execve 的时候

从栈顶到栈底依次是:main 未来的栈帧 -> libc_start_main 的栈帧 -> argv[0], argv[1], … -> envp[0], envp[1], … -> argv 的内容 -> envp 的内容(字符串)


SIGKILL 和 SIGSTOP 不能被捕获/忽略。

内核给每个进程维护一个 pending 位向量。接收到一个就把 pending 的对应位置 1,接收一个就置 0。

进程可以通过 signal 函数修改收到信号的行为,注意只管当前进程不管父进程。

一个 handler 在运行的时候可能接收到信号,那么就会转到另一个 handler。

  • 在 handler 中只调用异步信号安全的函数printfmallocexit 都不行。**唯一安全输出方法是用 write
  • 在 handler 里面保存并恢复 errno
  • 如果要访问共享全局数据结构,暂时阻塞所有信号
  • volatile 声明全局变量(不会被缓存到寄存器中)
  • sig_atomic_t 声明
  • 不要利用 handler 做给信号计数之类的工作。一来要视为来了一车。while (waitpid)

显式等待信号(shell 的父进程等待子进程结束 SIGCHLD)

基本思路是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
volatile sig_atomic_t pid;

void sigchld_handler(int s) {
int olderrno = errno;
pid = waitpid(-1, NULL, 0);
errno = errno;
}

int main() {
...
while (1) {
Sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞 SIGCHLD
if (Fork() == 0) {
exit(0);
}
// parent
pid = 0;
Sigprocmask(SIG_SETMASK, &prev, NULL);
// 等待 SIGCHLD
while (!pid);

...
}
}

但是这个 while 很占用资源。如果改成 while(!pid) pause();竞争while 测试后且 pause 执行前收到信号的话,pause 就永远 pause 了)

sleep(1) 替代,也很浪费时间。

合理的方法是用 sigsuspend(mask)(阻塞 mask 的信号,收到信号后若是被杀就直接杀,若是 handler 就执行完后返回)。

改成 while (!pid) sigsuspend(&prev) 就可以了。


非本地跳转。

setjmp(env)env 保存当前调用环境(包括 PC,%rsp 和通用目的寄存器),返回值不能被赋值给变量但是可以用在 switch 或条件语句的测试longjmpenv 中恢复调用环境,然后触发一个从最近一次初始化 envsetjmp 的返回。

前者被调用一次返回多次,后者不返回。

Chap. 10 SysIO

Unix 万物皆文件,输入输出等价于对文件的读写。

每次打开文件都有唯一的非负 fd 与其对应。

  • 0 是 STDIN
  • 1 是 STDOUT
  • 2 是 STDERR

所以正常而言 fd 是从 3 开始分配的,每次分配最小的未被占用的

对于每个打开的文件,内核维护一个文件位置 kk,可以通过 seek 显式设置。

读文件:文件结尾没有显式的 EOF 记号


文件的分类:普通文件,目录文件,socket。


打开文件:int open(char* filename, int flags, mode_t mode),返回一个 fd。

flags 参数,也是位向量掩码。

  • O_RDONLY:只读
  • O_WRONLY:只写
  • o_RDWR:可读可写

额外指示:

  • O_CREAT:不存在就创建空的
  • O_TRUNC:存在就夹断
  • O_APPEND:写之前把文件位置设置到最尾

进程上下文包含 umaskmode 可以是S_I R/W/X USR/GRP/OTH,用 open 创建新文件的时候,其权限位会被设置成 mode & ~umask。默认情况下是拥有者有读写权限,其他用户只读。

close(fd) 可以关闭一个 fd,关一个已关闭的会出错

做题的时候注意 open 的参数是什么,是 O_TRUNC 或者 O_APPEND 的话都要格外注意。


RIO 包健壮读写

  • 无缓冲的 I/O 函数
  • 带缓冲的输入函数,没有带缓冲输出。

缓冲区的意义是减少系统调用 read 的次数。每次系统调用都要陷入内核,比用户调用函数慢。

不要用 rio_readline(b) 来读二进制文件。


statfstat 可以读取文件的 metadata。

st_size 文件大小,st_mode 是许可位和类型,可以用相关的宏判定文件类型。


opendir(name) 返回指向目录流的指针。

readdir 调用会返回指向下一个目录项的指针。


内核用三个数据结构维护打开的文件:

  • fd 表:每个进程都有自己的 fd 表。

  • 文件表:打开文件的集合,所有进程共享

    文件表表项:文件位置,refcnt,指向 v-node 表的指针

  • v-node 表:所有进程共享,每个表项包含 stat 结构中大部分信息。

注意:多个 fd 可以通过不同的文件表表项引用同一个文件,因为每个 fd 都有自己的文件位置,对不同 fd 操作就可以从文件不同位置读信息。

fork 之后,子进程会有和父进程一样的 fd 表,自然其引用的文件,refcnt 会增加。内核直到 refcnt00 才会真正关闭文件。


I/O 重定向:用 dup2(oldfd, newfd) 函数。

会把 oldfd 的 fd 表项复制到 newfd 的 fd 表项

相当于这个时候,oldfd 和 newfd 都对应了原来 oldfd 指向的文件,refcnt 会增加。

把 STDIN 重定向到 5 就是 dup(5, 0)

做题的时候千万看清楚哪个进程里,哪个文件表项,对应的文件位置是什么。不知道为什么出题的就喜欢在这上面整花活

Chap. 11 NetProg

client 和 server 都是进程

  • 最低层是局域网 LAN,常见技术 以太网。主机之间由集线器 hub连接,帧会被转发到每个主机
  • 网桥可以把若干以太网连起来形成桥接以太网。网桥有分配算法。
  • 多个不兼容的局域网通过路由器 router 连接成互联网 Internet(注意区分 Internet)

各种协议:

  • IP 协议:主机到主机数据报 datagram不可靠
  • UDP 协议:进程间,不可靠
  • TCP 协议:进程间,全双工

client 和 server 通过在连接上发送字节流来通信,点对点,全双工

接下来分别考虑 client 和 server 的套接字接口:

  • client:
    • getaddrinfo:传进去主机名,返回一个 ai_addr 链表。记得用 freeaddrinfo 给释放
    • socket:创建一个套接字 fd,在这里为 clientfd不能直接用于读写
    • connect:传入 clientfd 和待连接服务器的套接字地址 addr阻塞,直到连接成功或者失败。成功了就可以用 clientfd 来 IO 了。
    • 然后就用 rio_writenrio_readlineb 来进行通信。
    • close 来结束。
  • server:
    • getaddrinfo:主机名的地方NULL 进去,hints 里面的 ai_flags 要带上 AI_PASSIVE
    • socket 创建一个套接字 fd,一样的。
    • int bind(int sockfd, const struct sockaddr *addr, socketlent_t addrlen)socket 创建的 fd 与 getaddrinfo 搞到的 addr 给绑定起来
    • listen:把 sockfd主动套接字转化为监听套接字,注意:不会阻塞
    • int accept(int listenfd, sockaddr *addr, int *addrlen)阻塞,直到收到连接请求,把客户端的 sockaddr 填写到第二个参数里面,返回一个connfd,接下来用这个 fd 来通信
    • 然后就用 rio_writenrio_readlineb 来进行通信。read 到 EOF 了就结束。

getaddrinfogetnameinfo 二者是相反关系。注意其与具体协议无关。

注意 open_listenfdopen_clientfd 的实现,注意失败的情况一定要记得关闭对应的 fd

HTTP 是应用级协议。


ICS 期末复习
https://blog.imyangty.com/note-ics-final-review/
Author
YangTY
Posted on
December 22, 2024
Licensed under