[card title="概述" color="primary"]我们已经知道操作系统是支持应用程序运行的软件,定义了操作系统里的对象和操作它们的API

  • 但什么是应用程序?
  • 它们怎么样调用操作系统?[/card]

什么是应用程序?

(应用)程序

可执行文件(程序的二进制代码和数据)和其它数据文件

  • Linux支持多种可执行文件格式
  • ELF(Executable Linkable Format)是其中最常用的

运行的程序称为进(正在运行的)程(程序)

  • 操作系统中有很多进程对象
  • 在运行时,进程会

    • 在CPU上执行,进行计算
    • 使用操作系统API访问操作系统中的其它对象

系统中常见的应用程序

Core Utilities(coreutils)

  • standard programs for text and file manipulation
  • 系统中安装的是GNU Coreutils
    -有较小的替代品busybox

系统/工作程序

  • bash,apt,ip,ssh,vim,tmux,jdk,python,...

    • 有些工具的原理都不复杂,例如apt的主要功能是把文件复制到指定的地方;有时会执行脚本/trigger
    • Ubuntu Packages 支持文件名检索;因此缺少什么东西这是最佳STFW的入口

      • 例子:搜索SDL2/SDL.H

其它各种应用程序

  • 浏览器、音乐播放器...

ELF二进制文件

首先,可执行文件也是普通的文件

  • 操作系统里的一个对象
  • 一个存储在文件系统(通常是存储设备)上的字节序列

    • 与大家平时创建的文本文件(例如程序)没有本质区别
    • 操作系统提供API打开、读取、改写(都需要相应的权限)
    • 因此我们可以用vim,cat,xxd等命令查看可执行文件

      • 在vim中打开,二进制的部分显示异常,但可以看到字符串常量(例子:vim/bin/ls)
      • 使用xxd可以看到文件以"x7f" "ELF"开头

[card title="键入 xxd /bin/ls | less" color="primary"][/card]

解析ELF文件

readelf是专门解析ELF可执行文件的工具,我们要关注:

  • ELF文件的header(元数据)

    • 文件内容的分布
    • 指令集体系结构
    • 入口地址
    • ...

我们使用man readelf可以查看它的使用方法

查看file-header readelf -h /bin/ls

[card title="重要信息" color="primary"]Magic:7F 45 4C
Class:ELF64
Data:2补码 小端序
...
Entry point address:0x6130
...[/card]

查看program-header readelf -l /bin/ls

  • ELF的program headers

    • 决定ELF应该如何被加载器加载(执行)

如果需要用代码解析:/usr/include/elf.h 提供了必要的定义


应用程序(Hello World)怎么调用操作系统?

失败的尝试1

//希望实现一个操作系统上“最小的”Hello World
$ vim hello.c
#include<stdio.h>
int main(){
   printf("hello world\n");
}
$ gcc hello.c
$ ./a.out
hello world
$ gcc -c hello.c //生成.o目标文件
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
//ld 用于把目标代码文件链接为可执行文件或者库文件
$ ld hello.o
ld: 警告: 无法找到项目符号 _start; 缺省为 0000000000401000
ld: hello.o: in function `main':
hello.c:(.text+0xc): undefined reference to `puts'

为什么?
为什么是puts,明明写了printf。

  • gcc在-O0选项下依然会进行一定程度上的编译优化
  • 导致一些编译器bugs的源头

为了省去printf检查的开销,printf被优化成了puts,为什么undefined reference to `puts'?

  • puts是库函数(libc)
  • 但如果把库也链接进来(gcc hello.c)就达不到“最小”了

警告: 无法找到项目符号 _start;(cannot find entry symbol_start?)

  • _start是链接器默认的入口
  • 可以用-e指定,比如-e main printf(被优化成的puts)是库函数的一部分

[card title="两种方法绕过这个“警告”" color="primary"]

  • int main → int _start
  • ld -e main printf 用-e选项指定链接器的入口

[/card]

失败的尝试2

//如果不链接任何标准库
int main(){
}
$ gcc -c hello.c
//objdump -d test  反汇编test中的需要执行指令的那些section
$ objdump -d hello.o 
hello.o:     文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
   0:    55                       push   %rbp
   1:    48 89 e5                 mov    %rsp,%rbp
   4:    b8 00 00 00 00           mov    $0x0,%eax
   9:    5d                       pop    %rbp
   a:    c3                       retq  
$ ld -e main hello.o //没有报错警告
$ ./a.out
段错误

gdb调试
gdb使用手册
经过STFW/RTFM

  • starti可以帮助我们从第一条指令开始执行程序
  • layout asm可以更方便地查看汇编
  • info register可以查看寄存器
  • 指令讲解

gdb a.out
starti在程序第一条指令停下来

显示汇编layout asm
单步si直到ret,从栈上弹出,返回地址,配对call指令;如果有一条call指令,会把返回的地址放在堆栈上,让main执行,可是main函数是谁调用的呢?

列出调用栈backtracebt

可以看出栈上只有main,所以继续单步,则会报错Cannot access memory at address 0x1,发生了非法的内存访问,栈上含有一些其它的数值,而这些作为返回地址来说是不合法的。

操作系统做了什么?

  • 加载程序,并初始化运行环境(寄存器、代码、数据、堆栈)
  • 从_start开始执行

成功的尝试:汇编
main()函数的确开始运行了,只是返回时crash了

  • 我们只要写出正确的指令序列,就能在操作系统上正常执行了
  • 例子:minimal.S
#See also: man 2 syscall

.globl foo
foo:
    movl $1,        %eax 
    movl $1,        %edi 
    movq $s,        %rsi 
    movl $(e-s),    %edx 
    syscall             

    movl $60, %eax    
     movl $1,  %edi
    syscall

s:
    .ascii "\033[01;31mHello,OS World\033[0m\n"
e:
$ gcc -c minimal.S
$ ld -e foo minimal.o
$ ./a.out
Hello,OS World

objdump -d a.out

同样,我们用gdb调试a.out

先是mov指令,设置参数,来到syscall,调用系统API执行,打印Hello OS World

  • 执行了100%代码
  • 调用API打印Hello World

    • 访问的对象:编号为1的文件描述符
    • 执行的操作:写入一串字节序列
  • 调用API退出

如果使用C,应用程序得链接C的标准库,用syscall指令调用SYS_write ,往编号1的文件描述符里,写入地址为hello缓冲区上,length(hello)长的字节。

#include<unistd.h>
#include<sys/syscall.h>
#define LENGTH(arr) (sizeof(arr)/sizeof(arr[0]))
const char hello[]="\033[01;31mHello,OS World\033[0m\n";

int main(){
   syscall(SYS_write,1,hello,LENGTH(hello));
   syscall(SYS_exit,1);
}

syscall的代码在哪里?
使用objdump命令查看

  • object(目标文件)dump
  • "displays information about one or more object files"
  • -d:disassemble;-S:source(需要-g选项支持)
$ man objdump
objdump - display information from object files.
[-d|--disassemble]
[-S|--source]
[-g|--debugging]

如果用-g选项编译,可以把源代码也包装到二进制文件里,生成 debuginfo

$ gcc -g minimal1.c
$ objdump -S a.out | less

<syscall@plt>-动态链接(来自libc),说明main()在执行之前,程序已经执行过多次了,main()之前发生了什么,一个普通的C程序执行的第一条指令在哪里?
首先肯定不是main的第一条指令,是不是在libc的_start中?

$ gdb a.out
$ starti 
...
0x00007ffff7fd6090 in _start () from /lib64/ld-linux-x86-64.so.2
$ info registers //此时寄存器的状态
$ info inferiors //当前进程的地址空间里有什么
  Num  Description       Executable        
* 1    process 4876      /root/a.out
$ !pmap 4876 //暂停打印
4876:   /root/a.out
0000555555554000      4K r---- a.out
0000555555555000      4K r-x-- a.out
0000555555556000      4K r---- a.out
0000555555557000      8K rw--- a.out
00007ffff7fd0000     12K r----   [ anon ]
00007ffff7fd3000      8K r-x--   [ anon ]
00007ffff7fd5000      4K r---- ld-2.28.so 
00007ffff7fd6000    120K r-x-- ld-2.28.so
00007ffff7ff4000     32K r---- ld-2.28.so
00007ffff7ffc000      8K rw--- ld-2.28.so
00007ffff7ffe000      4K rw---   [ anon ]
00007ffffffde000    132K rw---   [ stack ]
 total              340K

ld-2.28.so就是最初始的加载器,帮我们加载libc,在调用libc的初始化,再调用main。

操作系统在运行一个C程序的时候,事实上经历了漫长的过程,如果我们想写一个程序直接在操作系统上加载执行,就得用类似汇编的方式;同时,main()的开始结束并不是整个程序的开始结束,在举出一个例子

//如果函数被设定为constructor属性,则该函数会在main函数执行之前被自动的执行;若函数被设定为destructor属性,则该函数会在main函数执行之后或者exit被调用后被自动的执行。
#include<stdio.h>
__attribute__((constructor)) void hello(){
   printf("Hello,World\n");
}
__attribute__((destructor)) void goodbye(){
   printf("Goodbye,Cruel OS World\n");
}

int main(){
}

Trace

main()执行之前,发生了哪些操作系统API调用,通过strace理解程序运行时用到的系统调用
CentOS/EulerOS系统 yum install strace
Ubuntu系统 apt-get install strace
用strace执行a.out strace ./a.out(或者strace ./a.out > /dev/null,标准输出重定向到/dev/null),我们发现execve,access等很多系统调用;本质上,所有程序和Hello World类似

  • 被操作系统加载

    • 通过父进程的execve
  • 不断执行系统调用

    • 进程管理:fork,execve,exit,...
    • 文件/设备管理:open,close,read,write,...
    • 存储管理:mmap,brk,...
  • 直到_exit(exit_group)退出


编译器gcc

  • strace -f gcc a.c(gcc会启动其他程序)

    • 主要的系统调用:execve,read,write
    • 执行程序:

      • cc1 -编译器(C→汇编)
      • as -汇编器(汇编→ELF relocatable)
      • collect2 -收集器(收集构造函数信息)
      • ld -链接