一个重新理解程序的角度 (enlightning)
程序 = 状态机 : 每个程序可以看做一个状态机
可以用状态机和状态转移分析来分析程序的正确性
初始状态是程序启动时操作系统给安排的状态
状态转移=时钟驱动的指令执行
假设当前状态是 (M, R)
从 R[PC]
取出一条指令
解析指令,取出必要的数据
计算结果(可能具有非确定性,例如rdrand
)
更新得到(M', R')
构造一个最小的hello world
失败的尝试
first try
#include <stdio.h> int main () { printf ("hello world\n" ); }
编译hello world程序,可以看到编译出来的a.out文件是非常大的
jones@DESKTOP-60SP3JA:~/jyyos/lec2/try1$ gcc hello.c jones@DESKTOP-60SP3JA:~/jyyos/lec2/try1$ ./a.out hello world jones@DESKTOP-60SP3JA:~/jyyos/lec2/try1$ ls -al total 32 drwxr-xr-x 2 jones jones 4096 Oct 15 12:59 . drwxr-xr-x 3 jones jones 4096 Oct 15 12:59 .. -rwxr-xr-x 1 jones jones 16696 Oct 15 12:59 a.out -rw-r--r-- 1 jones jones 59 Oct 15 12:59 hello.c
看a.out反汇编结果, 指令很多
这不符合最小hello world的需求,我们决定放弃gcc hello.c的编译方式
直接用ld链接
得到hello.o文件
再链接hello.o
链接失败,错误信息是不知道怎么链接函数库
使用空的main函数
好家伙,直接不要printf了,使用空的main函数,再编译
链接成功,只是报了一个找不到entry _start的warning
修改代码,将main()改成_start(), 就可以消除这个warning
int _start() { return 1 ; }
反汇编得到的指令非常少,确实满足了最小程序这个原则
然而,运行这个程序,段错误了
调试segmentation fault
我们需要一个工具帮助我们观察程序(状态机)的执行!
用gdb来调试一下段错误吧
gdb a.out
启动gdb调试
starti
转到第一条指令
(gdb) starti Starting program: /home/jones/jyyos/lec2/try1/a.out Program stopped. 0x0000000000401000 in _start () (gdb)
查看初始寄存器状态
(gdb) info registers ... rsp 0x7fffffffe400 0x7fffffffe400 ... rip 0x401000 0x401000 <_start> eflags 0x200 [ IF ] ... (gdb)
可以用x/i
打印$rip
中的指令
(gdb) x/i $rip => 0x401000 <_start>: endbr64
查看当前内存分配情况
首先用info inferiors
查看当前进程进程号
(gdb) info inferiors Num Description Executable * 1 process 5964 /home/jones/jyyos/lec2/try1/a.out
用linux的cat
命令查看当前进程的内存分配情况
(gdb) !cat /proc/5964/maps 00400000-00401000 r--p 00000000 08:40 176949 /home/jones/jyyos/lec2/try1/a.out 00401000-00402000 r-xp 00001000 08:40 176949 /home/jones/jyyos/lec2/try1/a.out 00402000-00403000 r--p 00002000 08:40 176949 /home/jones/jyyos/lec2/try1/a.out 7ffff7ffa000-7ffff7ffe000 r--p 00000000 00:00 0 [vvar] 7ffff7ffe000-7ffff7fff000 r-xp 00000000 00:00 0 [vdso] 7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack] (gdb)
可以看到00400000-00401000是一段只读的内存
00401000-00402000是一段可读可执行的内存
# 访问没有分配的内存地址 (gdb) x 0x1234 0x1234: Cannot access memory at address 0x1234 # 访问在映射中的地址 (gdb) x 0x400000 0x400000: 0x7f
什么时候发生了seg fault
layout asm
切换到汇编视图
用si
单步执行,发现在retq这里发生了seg fault
(gdb) si 0x0000000000000001 in ?? () Cannot access memory at address 0x1 (gdb)
retq这一步发生什么事了
retq这条汇编指令做了什么工作
我们回到retq执行前一步, 看调用retq前状态机所处的状态
查看rsp寄存器的值
(gdb) x/x $rsp 0x7fffffffe400: 0x00000001
查看之前的内存映射,这个程序不能访问这个地址,因此访问M[$rsp]
时就会失败
不想异常退出,那该怎么做
让操作系统参与进来
#include <sys/syscall.h> #include <unistd.h> int main () { syscall (SYS_exit, 42 ); }
编译运行成功,用echo $?
查看返回值得到42,这是系统调用的参数
jones@DESKTOP-60SP3JA:~/jyyos/lec2/try2$ gcc hello.c jones@DESKTOP-60SP3JA:~/jyyos/lec2/try2$ ./a.out jones@DESKTOP-60SP3JA:~/jyyos/lec2/try2$ echo $? 42
再用gdb进行调试,运行到syscall系统调用函数之前
查看此时寄存器状态
(gdb) info registers rax 0x3c 60 ... rdi 0x2a 42
执行这条syscall指令, 退出程序
(gdb) si [Inferior 1 (process 21950) exited with code 052]
Minimal Hello World (汇编实现)
#include <sys/syscall.h> .globl _start _start: movq $SYS_write, %rax # write( movq $1, %rdi # fd=1, movq $st, %rsi # buf=st, movq $(ed - st), %rdx # count=ed-st syscall # ); movq $SYS_exit, %rax # exit( movq $1, %rdi # status=1 syscall # ); st: .ascii "\033[01;31mHello, OS World\033[0m\n" ed:
编译运行
[jones@DESKTOP-60SP3JA:~/jyyos/lec2/try3$ gcc -c minimal.S jones@DESKTOP-60SP3JA:~/jyyos/lec2/try3$ ls minimal.S minimal.o jones@DESKTOP-60SP3JA:~/jyyos/lec2/try3$ ld minimal.o jones@DESKTOP-60SP3JA:~/jyyos/lec2/try3$ ./a.out Hello, OS World
输出了一个红色的hello world
可以用gdb调试一下,流程和之前一致
操作系统中的程序
回到之前程序=状态机(M, R)的理解
程序中的指令大致可以分为两种
内部指令: add
, call
, ret
确定的: 完全根据当前(M, R)计算出一个新的(M, R)
非确定的:rdrand
外部指令(只有一条)
syscall: 交给操作系统,让操作系统决定下一个状态是什么
除了可以观察syscall返回的结果,程序完全不知道syscall发生了什么
为什么要这么做?为什么要操作系统, 为什么程序有唯一的对os api的调用
操作系统实现了资源的同一管理。
程序只能用操作系统允许的方式访问操作系统中的对象,从而实现操作系统的霸主地位。
这是为管理多个状态机所必须的,不能打架,谁有权限就给谁
程序作为操作系统中的对象
可执行文件=字节序列
可执行文件和文本文件(比如程序)没有任何区别
操作系统提供API打开,读取,改写 (都需要响应的权限)
查看/bin/ls文件
jones@DESKTOP-60SP3JA :~/jyyos/lec2/try3$ file /bin/ls /bin/ls : ELF 64 -bit LSB shared object, x86-64 , version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux -x86 -64 .so.2 , BuildID[sha1 ]=2 f15ad836be3339dec0e2e6a3c637e08e48aacbd, for GNU/Linux 3.2 .0 , stripped jones@DESKTOP-60SP3JA :~/jyyos/lec2/try3$ xxd /bin/ls | less 00000000 : 7 f45 4 c46 0201 0100 0000 0000 0000 0000 .ELF............00000010 : 0300 3 e00 0100 0000 d067 0000 0000 0000 ..>......g...........
一些真实的例子
一个普通的C程序执行的第一条指令在哪里
main的第一条指令? ×
libc的_start? ×
试试呗
(gdb) starti Starting program: /tmp/a.out Program stopped. 0x00007ffff7dd6090 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) bt # 0 0x00007ffff7dd6090 in _start () from /lib64/ld-linux-x86-64.so.2 # 1 0x0000000000000001 in ?? ()
第一条指令是动态链接的加载器
jones@DESKTOP-60SP3JA:~/jyyos/lec2/try3$ readelf -a /bin/ls |less
ld-linux-x86-64.so加载了libc
之后libc完成了自己的初始化
main()函数的开始/结束不是整个程序的开始/结束
#include <stdio.h> __attribute__((constructor)) void hello () { printf ("Hello, World\n" ); } __attribute__((destructor)) void goodbye () { printf ("Goodbye, Cruel OS World!\n" ); } int main () { printf ("Main\n" ); } jones@DESKTOP-60 SP3JA:~/jyyos/lec2/try4$ ./a.out Hello, World Main Goodbye, Cruel OS World!
main函数在执行之前,执行中,执行后,发生了哪些操作系统API调用
一个非常重要的工具: strace
(system call trace)
输出程序运行时使用户的系统调用
jones@DESKTOP-60SP3JA:~/jyyos/lec2/try4$ strace ./a.out | less execve("./a.out", ["./a.out"], 0x7ffd63bf5f30 /* 22 vars */) = 0 brk(NULL) = 0x55ddb7ce3000 arch_prctl(0x3001 /* ARCH_??? */, 0x7fff04c654a0) = -1 EINVAL (Invalid argument) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=44804, ...}) = 0 mmap(NULL, 44804, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f4b9010d000 close(3) = 0 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832 pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784 pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32 pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68 fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4b9010b000 ...
本质上,所有的程序和hello world类似
被操作系统加载
状态机执行(内部+唯一的外部指令syscall)
进程管理: fork, execve, exit …
文件/设备管理: open, close, read, write …
存储管理: mmap, brk
直到_exit(exit_group)退出
总结