LLVM 环境搭建

LLVM相关

环境搭建

PC

VMware Workstation

​ 下载: https://www.vmware.com/go/getworkstation-win

​ KEY: ZC3WK-AFXEK-488JP-A7MQX-XL8YF (可自行网上查询或购买正版)

Ubuntu16.04.iso

​ 下载: http://releases.ubuntu.com/16.04/ubuntu-16.04.3-desktop-amd64.iso

首先优雅地开启VMwarWorkstation软件, 按照下方图文操作.

img

选择新建虚拟机

如果想要自己完整的安装ubuntu系统则需要选择"自定义", 想懒省事话选择"典型", 不过默认是英文环境.

img

选择自定义

img

兼容性, 直接点下一步

这里选择稍后安装系统, 也可以选择第二项

img

选择稍后安装

img

选择好对应系统和版本

img

设置名称(不是虚拟机系统内的用户名)和系统文件存储位置

这里根据自己电脑配置选择, 我的CPU是I7 7700HQ, 4核8线程, 所以按照一半来设置

处理器数量: 核心数

内核数量: 线程

img

选择CPU数量

内存同样按照一半来配置,可根据自身主要使用哪个系统(PC/虚拟机)来进行配置

img

选择内存大小

网络连接方式使用默认的即可, 后期有需要可在"虚拟机"->"设置"中更改,这里不多作解释.

img

选择网络连接方式

img

IO选择,默认

img

磁盘类型,同样默认

img

默认

勾选"立即分配所有磁盘空间"会导致在在设置完成后花费较长时间分配完整的50G空间, 如果没有特殊需求, 不推荐.

单/多个文件可根据自己选择,但总的来说,默认就好.

img

对磁盘大小进行设置, 其它默认

img

虚拟机启动文件,默认

img

按下"完成"按钮

在左则"库"中选中虚拟机, 如果没有"库"的话, 在"查看" -> "自定义"中选择"库(F9)

点击标签页中的"编辑虚拟机设置" 或 菜单栏的"虚拟机" -> “设置”

img

打开设置窗口

然后,把系统镜像文件对准空壳的小口, 我们要开始往里塞了!

img

选择镜像文件

img

启动虚拟机

语言列表拉到最下选择中文, 也可以默认使用英文环境.

如果安装的时候选择了中文, 安装好后用户文件夹名称也是中文, 这对撸代码不利, 不过有可以将文件夹切换回英文的方法, 以后会说.

img

选择系统语言并开始安装系统

如果不选择"安装时下载更新"会节约安装时间, 但安装后好后系统肯定会提示更新(除非离线), 这个自行定夺.

关于"第三方软件", 如果不拿它当日常主力系统的话(你拿虚拟机当日常系统使用吗?),可以不勾选, 这项也可以安装好系统之后看到.

img

我们已经创造了存放系统的空壳, 自然是专用的啦.

img

清除杂念, 开始安装

img

默认

时区选择, 貌似中国这一块点哪都显示的是"Shanghai", 所以不用在意.

img

选择时区

img

默认啦

img

设置好你的用户账户

img

VMware

用VMware菜单里的功能安装

img

点击安装

这里会有一个提示, 现在虚拟机的光驱里应该塞的是Ubuntu的镜像, 而安装Tools需要从光驱中安装,所以要把镜像拔出来

img

拔出! 插入!

然后会在启动栏中看到光驱图标, 或者在文件中看到内容

img

文件-> VMware Tools, 或者 直接点击DVD图标

接下来, 我们要使用这个安装包

img

Tools的安装包

把它移动到其它地方, 可以使用常规的复制粘贴, 或者使用终端命令行来执行:

\1. CTRL + ALT + T 打开终端

\2. 输入:cd /media/[用户名]/VMware Tools/

(善用TAB自动补全! 输入cd /me{tab}pat{tab}VM{tab} 就可以打出需要的路径)

\3. 输入: cp VM{tab}{空格}/home/pat{tab}Doc{tab} # 这里将安装包拷贝到文档目录下

\4. 切换到文档目录: cd /home/pat{tab}Doc{tab} 并查看有没有成功: ls

img

将安装包拷出来

\5. 输入 tar -xzvf VM{tab} ./IS

#tar和参数可自行查询, 目前只需要明白解压tar.gz文件需要这个命令即可

\6. 一长串信息刷屏后, 文件夹中会出现另一个文件夹(vmware-tools-distrib), 我们进入

img

进入解压后的文件夹

\7. 输入: sudo ./vm{tab}

8.安装的时候会出现各种确认信息, 如果是[no]或[yes]就按一下y再回车, 如果只是文件路径, 直接回车.

img

出现[no]或者[yes]就输入一个y

img

直接回车即可

img

输入y

img

断断续续过后, 安装完成~

\9. 重启大法!

\10. 接下来可以随意使用拖拽, CTRL+XVC,或者右键来移动复制文件啦~

Root

1.首先设置root用户密码:

**sudo passwd root**

输入普通用户密码,再输入root用户密码;

2.启用登录时的root选项:

编辑50-ubuntu.conf文件:

**sudo gedit /usr/share/lightdm/lightdm.conf.d/50-ubuntu.conf**

添加:

greeter-show-manual-login=true

编辑/root/.profile文件:

**sudo gedit /root/.profile**

找到 mesg n这一行,修改为:

tty -s && mesg n

保存退出~

3.配置root自动登陆:

编辑lightdm.conf文件:

**sudo gedit  /etc/lightdm/lightdm.conf**

添加如下内容:

[SeatDefaults]
autologin-user=root
greeter-session=unity-greeter
user-session=ubuntu
greeter-show-manual-login=true
allow-guest=false

保存重启完事~

LLVM release

用 sh -x test.sh 执行脚本就会这样,这是进入了调试模式,会打印每行被执行到的代码。
或者脚本里被加入了 set -x 和 set +x 调试开关。

遇到 vi 编辑器的上下左右方向金变成ABCD 时:

解决方法:

 cp /etc/vim/vimrc  ~/.vimrc  

脚本安装

apt install vim
vim install_llvm.sh
#!/bin/bash
set -eux

LINUX_VER=${LINUX_VER:-ubuntu-16.04}
LLVM_VER=${LLVM_VER:-7.0.0}
PREFIX=${PREFIX:-${HOME}}

LLVM_DEP_URL=https://releases.llvm.org/${LLVM_VER}
TAR_NAME=clang+llvm-${LLVM_VER}-x86_64-linux-gnu-${LINUX_VER}

wget -q ${LLVM_DEP_URL}/${TAR_NAME}.tar.xz
tar -C ${PREFIX} -xf ${TAR_NAME}.tar.xz
rm ${TAR_NAME}.tar.xz
mv ${PREFIX}/${TAR_NAME} ${PREFIX}/clang+llvm

set +x
echo "Please set:"
echo "export PATH=\$PREFIX/clang+llvm/bin:\$PATH"
echo "export LD_LIBRARY_PATH=\$PREFIX/clang+llvm/lib:\$LD_LIBRARY_PATH"

chmod 777 install_llvm.sh
./install_llvm.sh
export PATH=/root/clang+llvm/bin:\$PATH
export LD_LIBRARY_PATH=/root/clang+llvcm/lib:\$LD_LIBRARY_PATH
apt update


getdit  /etc/environment
    PATH=    :/root/clang+llvm/bin"
    LD_LIBRARY_PATH="/root/clang+llvcm/lib"
source /etc/environment
reboot
root@ty-virtual-machine:~# clang -v
clang version 7.0.0 (tags/RELEASE_700/final)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /root/clang+llvm/bin
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/5
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/5.4.0
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/6
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/6.0.0
Selected GCC installation: /usr/lib/gcc/x86_64-linux-gnu/5.4.0
Candidate multilib: .;@m64
Selected multilib: .;@m64

root@ty-virtual-machine:~# vim helloworld.c
        //helloworld.c
        #include <stdio.h>
        int main() {
            printf("hello world\n");
            return 0;
        }
root@ty-virtual-machine:~# clang helloworld.c -o hello.out
root@ty-virtual-machine:~# ./hello.out
hello world
root@ty-virtual-machine:~# vim helloworld.cpp
    	//helloworld.cpp
        #include <iostream>
        using namespace std;
        int main() {
            cout << "hello world" << endl;
            return 0;
        }
root@ty-virtual-machine:~# clang++ helloworld.cpp -o hello.out
root@ty-virtual-machine:~# ./hello.out
hello world

Clang(Clang++)使用

我们先随便写一段以下代码:

//test.cpp
#include <iostream>
#include <algorithm>

using namespace std;

int a[10] = {4,2,7,5,6,1,8,9,3,0};

int main() {
	for(int i = 0; i < 10; ++i)
		cout << a[i] << (i == 9?"\n":" ");
	sort(a,a+10);
	for(int i = 0; i < 10; ++i)
		cout << a[i] << (i == 9?"\n":" ");
	return 0;
}

3.1.生成预处理文件:

$ clang++ -E test.cpp -o test.i

3.2.生成汇编程序:

$ clang++ -S test.i

LLVM

3.3.生成目标文件:

$ clang++ -c test.s

LLVM

3.4.生成可执行文件:

$ clang++ -o test.out test.o

LLVM

emm…和GCC大致还是一样的嘛 非常的好用。

3.5.查看Clang编译的过程

$ clang -ccc-print-phases A.c

LLVM

  • 0.获取输入:A.c文件,C语言
  • 1.预处理器:处理define、include等
  • 2.编译:生成中间代码(IR)
  • 3.后端:生成汇编代码
  • 4.汇编:生成目标代码
  • 5.链接器:链接其他动态库

3.6.词法分析

$ clang -fmodules -E -Xclang -dump-tokens A.c

如图,写一个小函数对其进行词法分析。

3.7.语法分析

$ clang -fmodules -fsyntax-only -Xclang -ast-dump A.c

生成语法树如下:

有颜色区分还是比较美观的。

3.8.语义分析

生成LLVM IR。LLVM IR有3种表示形式(本质是等价的)

  • (1).text:便于阅读的文本格式,类似于汇编语言,拓展名.ll
  • (2).memory:内存格式
  • (3).bitcode:二进制格式,拓展名.bc

生成text格式:

$ clang -S -emit-llvm A.c

1.3 Linux 基础c

常用基础命令

ls                  用来显示目标列表
cd [path]           用来切换工作目录
pwd                 以绝对路径的方式显示用户当前工作目录
man [command]       查看Linux中的指令帮助、配置文件帮助和编程帮助等信息
apropos [whatever]  在一些特定的包含系统命令的简短描述的数据库文件里查找关键字
echo [string]       打印一行文本,参数“-e”可激活转义字符
cat [file]          连接文件并打印到标准输出设备上
less [file]         允许用户向前或向后浏览文字档案的内容
mv [file1] [file2]  用来对文件或目录重新命名,或者将文件从一个目录移到另一个目录中
cp [file1] [file2]  用来将一个或多个源文件或者目录复制到指定的目的文件或目录
rm [file]           可以删除一个目录中的一个或多个文件或目录,也可以将某个目录及其下属的所有文件及其子目录均删除掉
ps                  用于报告当前系统的进程状态
top                 实时查看系统的整体运行情况
kill                杀死一个进程
ifconfig            查看或设置网络设备
ping                查看网络上的主机是否工作
netstat             显示网络连接、路由表和网络接口信息
nc(netcat)          建立 TCP 和 UDP 连接并监听
su                  切换当前用户身份到其他用户身份
touch [file]        创建新的空文件
mkdir [dir]         创建目录
chmod               变更文件或目录的权限
chown               变更某个文件或目录的所有者和所属组
nano / vim / emacs  字符终端的文本编辑器
exit                退出 shell

使用变量:

var=value         给变量var赋值value
$var, ${var}      取变量的值
`cmd`, $(cmd)     代换标准输出
'string'          非替换字符串
"string"          可替换字符串

$ var="test";
$ echo $var
test
$ echo 'This is a $var';
This is a $var
$ echo "This is a $var";
This is a test
$ echo `date`;
2017年 11月 06日 星期一 14:40:07 CST
$ $(bash)
$ echo $0
/bin/bash
$ $($0)

Bash 快捷键

Up(Down)          上(下)一条指令
Ctrl + c          终止当前进程
Ctrl + z          挂起当前进程,使用“fg”可唤醒
Ctrl + d          删除光标处的字符
Ctrl + l          清屏
Ctrl + a          移动到命令行首
Ctrl + e          移动到命令行尾
Ctrl + b          按单词后移(向左)
Ctrl + f          按单词前移(向右)
Ctrl + Shift + c  复制
Ctrl + Shift + v  粘贴

更多细节请查看:Bash Keyboard Shortcuts

根目录结构

$ uname -a
Linux manjaro 4.11.5-1-ARCH #1 SMP PREEMPT Wed Jun 14 16:19:27 CEST 2017 x86_64 GNU/Linux
$ ls -al /
drwxr-xr-x  17 root root  4096 Jun 28 20:17 .
drwxr-xr-x  17 root root  4096 Jun 28 20:17 ..
lrwxrwxrwx   1 root root     7 Jun 21 22:44 bin -> usr/bin
drwxr-xr-x   4 root root  4096 Aug 10 22:50 boot
drwxr-xr-x  20 root root  3140 Aug 11 11:43 dev
drwxr-xr-x 101 root root  4096 Aug 14 13:54 etc
drwxr-xr-x   3 root root  4096 Apr  8 19:59 home
lrwxrwxrwx   1 root root     7 Jun 21 22:44 lib -> usr/lib
lrwxrwxrwx   1 root root     7 Jun 21 22:44 lib64 -> usr/lib
drwx------   2 root root 16384 Apr  8 19:55 lost+found
drwxr-xr-x   2 root root  4096 Oct  1  2015 mnt
drwxr-xr-x  15 root root  4096 Jul 15 20:10 opt
dr-xr-xr-x 267 root root     0 Aug  3 09:41 proc
drwxr-x---   9 root root  4096 Jul 22 22:59 root
drwxr-xr-x  26 root root   660 Aug 14 21:08 run
lrwxrwxrwx   1 root root     7 Jun 21 22:44 sbin -> usr/bin
drwxr-xr-x   4 root root  4096 May 28 22:07 srv
dr-xr-xr-x  13 root root     0 Aug  3 09:41 sys
drwxrwxrwt  36 root root  1060 Aug 14 21:27 tmp
drwxr-xr-x  11 root root  4096 Aug 14 13:54 usr
drwxr-xr-x  12 root root  4096 Jun 28 20:17 var

由于不同的发行版会有略微的不同,我们这里使用的是基于 Arch 的发行版 Manjaro,以上就是根目录下的内容,我们介绍几个重要的目录:

  • /bin/sbin:链接到 /usr/bin,存放 Linux 一些核心的二进制文件,其包含的命令可在 shell 上运行。
  • /boot:操作系统启动时要用到的程序。
  • /dev:包含了所有 Linux 系统中使用的外部设备。需要注意的是这里并不是存放外部设备的驱动程序,而是一个访问这些设备的端口。
  • /etc:存放系统管理时要用到的各种配置文件和子目录。
  • /etc/rc.d:存放 Linux 启动和关闭时要用到的脚本。
  • /home:普通用户的主目录。
  • /lib/lib64:链接到 /usr/lib,存放系统及软件需要的动态链接共享库。
  • /mnt:这个目录让用户可以临时挂载其他的文件系统。
  • /proc:虚拟的目录,是系统内存的映射。可直接访问这个目录来获取系统信息。
  • /root:系统管理员的主目录。
  • /srv:存放一些服务启动之后需要提取的数据。
  • /sys:该目录下安装了一个文件系统 sysfs。该文件系统是内核设备树的一个直观反映。当一个内核对象被创建时,对应的文件和目录也在内核对象子系统中被创建。
  • /tmp:公用的临时文件存放目录。
  • /usr:应用程序和文件几乎都在这个目录下。
  • /usr/src:内核源代码的存放目录。
  • /var:存放了很多服务的日志信息。

进程管理

  • top
    • 可以实时动态地查看系统的整体运行情况。
  • ps
    • 用于报告当前系统的进程状态。可以搭配 kill 指令随时中断、删除不必要的程序。
    • 查看某进程的状态:$ ps -aux | grep [file],其中返回内容最左边的数字为进程号(PID)。
  • kill
    • 用来删除执行中的程序或工作。
    • 删除进程某 PID 指定的进程:$ kill [PID]

UID 和 GID

Linux 是一个支持多用户的操作系统,每个用户都有 User ID(UID) 和 Group ID(GID),UID 是对一个用户的单一身份标识,而 GID 则对应多个 UID。知道某个用户的 UID 和 GID 是非常有用的,一些程序可能就需要 UID/GID 来运行。可以使用 id 命令来查看:

$ id rootuid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),19(log)$ id firmyuid=1000(firmy) gid=1000(firmy) groups=1000(firmy),3(sys),7(lp),10(wheel),90(network),91(video),93(optical),95(storage),96(scanner),98(power),56(bumblebee)

UID 为 0 的 root 用户类似于系统管理员,它具有系统的完全访问权。我自己新建的用户 firmy,其 UID 为 1000,是一个普通用户。GID 的关系存储在 /etc/group 文件中:

$ cat /etc/grouproot:x:0:rootbin:x:1:root,bin,daemondaemon:x:2:root,bin,daemonsys:x:3:root,bin,firmy......

所有用户的信息(除了密码)都保存在 /etc/passwd 文件中,而为了安全起见,加密过的用户密码保存在 /etc/shadow 文件中,此文件只有 root 权限可以访问。

$ sudo cat /etc/shadowroot:$6$root$wvK.pRXFEH80GYkpiu1tEWYMOueo4tZtq7mYnldiyJBZDMe.mKwt.WIJnehb4bhZchL/93Oe1ok9UwxYf79yR1:17264::::::firmy:$6$firmy$dhGT.WP91lnpG5/10GfGdj5L1fFVSoYlxwYHQn.llc5eKOvr7J8nqqGdVFKykMUSDNxix5Vh8zbXIapt0oPd8.:17264:0:99999:7:::

由于普通用户的权限比较低,这里使用 sudo 命令可以让普通用户以 root 用户的身份运行某一命令。使用 su 命令则可以切换到一个不同的用户:

$ whoamifirmy$ su root# whoamiroot

whoami 用于打印当前有效的用户名称,shell 中普通用户以 $ 开头,root 用户以 # 开头。在输入密码后,我们已经从 firmy 用户转换到 root 用户了。

权限设置

在 Linux 中,文件或目录权限的控制分别以读取、写入、执行 3 种一般权限来区分,另有 3 种特殊权限可供运用。

使用 ls -l [file] 来查看某文件或目录的信息:

$ ls -l /lrwxrwxrwx   1 root root     7 Jun 21 22:44 bin -> usr/bindrwxr-xr-x   4 root root  4096 Jul 28 08:48 boot-rw-r--r--   1 root root 18561 Apr  2 22:48 desktopfs-pkgs.txt

第一栏从第二个字母开始就是权限字符串,权限表示三个为一组,依次是所有者权限、组权限、其他人权限。每组的顺序均为 rwx,如果有相应权限,则表示成相应字母,如果不具有相应权限,则用 - 表示。

  • r:读取权限,数字代号为 “4”
  • w:写入权限,数字代号为 “2”
  • x:执行或切换权限,数字代号为 “1”

通过第一栏的第一个字母可知,第一行是一个链接文件 (l),第二行是个目录(d),第三行是个普通文件(-)。

用户可以使用 chmod 指令去变更文件与目录的权限。权限范围被指定为所有者(u)、所属组(g)、其他人(o)和所有人(a)。

  • -R:递归处理,将指令目录下的所有文件及子目录一并处理;
  • <权限范围>+<权限设置>:开启权限范围的文件或目录的该选项权限设置
    • $ chmod a+r [file]:赋予所有用户读取权限
  • <权限范围>-<权限设置>:关闭权限范围的文件或目录的该选项权限设置
    • $ chmod u-w [file]:取消所有者写入权限
  • <权限范围>=<权限设置>:指定权限范围的文件或目录的该选项权限设置;
    • $ chmod g=x [file]:指定组权限为可执行
    • $ chmod o=rwx [file]:制定其他人权限为可读、可写和可执行

img

字节序

目前计算机中采用两种字节存储机制:大端(Big-endian)和小端(Little-endian)。

MSB (Most Significan Bit/Byte):最重要的位或最重要的字节。

LSB (Least Significan Bit/Byte):最不重要的位或最不重要的字节。

Big-endian 规定 MSB 在存储时放在低地址,在传输时放在流的开始;LSB 存储时放在高地址,在传输时放在流的末尾。Little-endian 则相反。常见的 Intel 处理器使用 Little-endian,而 PowerPC 系列处理器则使用 Big-endian,另外 TCP/IP 协议和 Java 虚拟机的字节序也是 Big-endian。

例如十六进制整数 0x12345678 存入以 1000H 开始的内存中:

img

我们在内存中实际地看一下,在地址 0xffffd584 处有字符 1234,在地址 0xffffd588 处有字符 5678

gdb-peda$ x/w 0xffffd584
0xffffd584:     0x34333231
gdb-peda$ x/4wb 0xffffd584
0xffffd584:     0x31    0x32    0x33    0x34
gdb-peda$ python print('\x31\x32\x33\x34')
1234
gdb-peda$ x/w 0xffffd588
0xffffd588:     0x38373635
gdb-peda$ x/4wb 0xffffd588
0xffffd588:     0x35    0x36    0x37    0x38
gdb-peda$ python print('\x35\x36\x37\x38')
5678
gdb-peda$ x/2w 0xffffd584
0xffffd584:     0x34333231      0x38373635
gdb-peda$ x/8wb 0xffffd584
0xffffd584:     0x31    0x32    0x33    0x34    0x35    0x36    0x37    0x38
gdb-peda$ python print('\x31\x32\x33\x34\x35\x35\x36\x37\x38')
123455678
db-peda$ x/s 0xffffd584
0xffffd584:     "12345678"

输入输出

  • 使用命令的输出作为可执行文件的输入参数
    • $ ./vulnerable your_command_here``
    • $ ./vulnerable $(your_command_here)
  • 使用命令作为输入
    • $ your_command_here | ./vulnerable
  • 将命令行输出写入文件
    • $ your_command_here > filename
  • 使用文件作为输入
    • $ ./vulnerable < filename

文件描述符

在 Linux 系统中一切皆可以看成是文件,文件又分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核管理已被打开的文件所创建的索引,使用一个非负整数来指代被打开的文件。

标准文件描述符如下:

文件描述符用途stdio 流
0标准输入stdin
1标准输出stdout
2标准错误stderr

当一个程序使用 fork() 生成一个子进程后,子进程会继承父进程所打开的文件表,此时,父子进程使用同一个文件表,这可能导致一些安全问题。如果使用 vfork(),子进程虽然运行于父进程的空间,但拥有自己的进程表项。

核心转储

当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存、寄存器状态、堆栈指针、内存管理信息等记录下来,保存在一个文件中,这种行为就叫做核心转储(Core Dump)。

会产生核心转储的信号

SignalActionComment
SIGQUITCoreQuit from keyboard
SIGILLCoreIllegal Instruction
SIGABRTCoreAbort signal from abort
SIGSEGVCoreInvalid memory reference
SIGTRAPCoreTrace/breakpoint trap

开启核心转储

  • 输入命令 ulimit -c,输出结果为 0,说明默认是关闭的。

  • 输入命令 ulimit -c unlimited 即可在当前终端开启核心转储功能。

  • 如果想让核心转储功能永久开启,可以修改文件 /etc/security/limits.conf,增加一行:

    #<domain>      <type>  <item>         <value>*               soft    core            unlimited
    

修改转储文件保存路径

  • 通过修改 /proc/sys/kernel/core_uses_pid,可以使生成的核心转储文件名变为 core.[pid] 的模式。

    # echo 1 > /proc/sys/kernel/core_uses_pid
    
  • 还可以修改 /proc/sys/kernel/core_pattern 来控制生成核心转储文件的保存位置和文件名格式。

    # echo /tmp/core-%e-%p-%t > /proc/sys/kernel/core_pattern
    

    此时生成的文件保存在 /tmp/ 目录下,文件名格式为 core-[filename]-[pid]-[time]

使用 gdb 调试核心转储文件

gdb [filename] [core file]

例子

$ cat core.c
#include <stdio.h>
void main(int argc, char **argv) {
    char buf[5];
    scanf("%s", buf);
}
$ gcc -m32 -fno-stack-protector core.c
$ ./a.out
AAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
$ file /tmp/core-a.out-12444-1503198911
/tmp/core-a.out-12444-1503198911: ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style, from './a.out', real uid: 1000, effective uid: 1000, real gid: 1000, effective gid: 1000, execfn: './a.out', platform: 'i686'
$ gdb a.out /tmp/core-a.out-12444-1503198911 -q
Reading symbols from a.out...(no debugging symbols found)...done.
[New LWP 12444]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x5655559b in main ()
gdb-peda$ info frame
Stack level 0, frame at 0x41414141:
 eip = 0x5655559b in main; saved eip = <not saved>
 Outermost frame: Cannot access memory at address 0x4141413d
 Arglist at 0x41414141, args:
 Locals at 0x41414141, Previous frame's sp is 0x41414141
Cannot access memory at address 0x4141413d

调用约定

函数调用约定是对函数调用时如何传递参数的一种约定。关于它的约定有许多种,下面我们分别从内核接口和用户接口介绍 32 位和 64 位 Linux 的调用约定。

内核接口

x86-32 系统调用约定:Linux 系统调用使用寄存器传递参数。eax 为 syscall_number,ebxecxedxesiebp 用于将 6 个参数传递给系统调用。返回值保存在 eax 中。所有其他寄存器(包括 EFLAGS)都保留在 int 0x80 中。

x86-64 系统调用约定:内核接口使用的寄存器有:rdirsirdxr10r8r9。系统调用通过 syscall 指令完成。除了 rcxr11rax,其他的寄存器都被保留。系统调用的编号必须在寄存器 rax 中传递。系统调用的参数限制为 6 个,不直接从堆栈上传递任何参数。返回时,rax 中包含了系统调用的结果。而且只有 INTEGER 或者 MEMORY 类型的值才会被传递给内核。

用户接口

x86-32 函数调用约定:参数通过栈进行传递。最后一个参数第一个被放入栈中,直到所有的参数都放置完毕,然后执行 call 指令。这也是 Linux 上 C 语言函数的方式。

x86-64 函数调用约定:x86-64 下通过寄存器传递参数,这样做比通过栈有更高的效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数的类型是 MEMORY,则在栈上传递参数。如果类型是 INTEGER,则顺序使用 rdirsirdxrcxr8r9。所以如果有多于 6 个的 INTEGER 参数,则后面的参数在栈上传递。

环境变量

环境变量字符串都是 name=value 这样的形式。大多数 name 由大写字母加下画线组成,一般把 name 部分叫做环境变量名,value 部分则是环境变量的值,而且 value 需要以 “/0” 结尾,环境变量定义了该进程的运行环境。

分类

  • 按照生命周期划分
    • 永久环境变量:修改相关配置文件,永久生效。
    • 临时环境变量:使用 export 命令,在当前终端下生效,关闭终端后失效。
  • 按照作用域划分
    • 系统环境变量:对该系统中所有用户生效。
    • 用户环境变量:对特定用户生效。

设置方法

  • 在文件 /etc/profile 中添加变量,这种方法对所有用户永久生效。如:

    # Set our default 
    pathPATH="/usr/local/sbin:/usr/local/bin:/usr/bin"
    export PATH
    

    添加后执行命令 source /etc/profile 使其生效。

  • 在文件 ~/.bash_profile 中添加变量,这种方法对当前用户永久生效。其余同上。

  • 直接运行命令 export 定义变量,这种方法只对当前终端临时生效。

常用变量

使用命令 echo 打印变量:

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
$ echo $HOME
/home/firmy
$ echo $LOGNAME
firmy
$ echo $HOSTNAME
firmy-pc
$ echo $SHELL
/bin/bash
$ echo $LANG
en_US.UTF-8

使用命令 env 可以打印出所有环境变量:

$ env
COLORFGBG=15;0
COLORTERM=truecolor
...

使用命令 set 可以打印出所有本地定义的 shell 变量:

$ set'!'=0'#'=0...

使用命令 unset 可以清除变量:

unset $变量名

LD_PRELOAD

该环境变量可以定义在程序运行前优先加载的动态链接库。在 pwn 题目中,我们可能需要一个特定的 libc,这时就可以定义该变量:

LD_PRELOAD=/path/to/libc.so ./binary

一个例子:

$ ldd /bin/true
  linux-vdso.so.1 =>  (0x00007fff9a9fe000)
  libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1c083d9000)
  /lib64/ld-linux-x86-64.so.2 (0x0000557bcce6c000)
$ LD_PRELOAD=~/libc.so.6 ldd /bin/true
  linux-vdso.so.1 =>  (0x00007ffee55e9000)
  /home/firmy/libc.so.6 (0x00007f4a28cfc000)
  /lib64/ld-linux-x86-64.so.2 (0x000055f33bc50000)

注意,在加载动态链接库时需要使用 ld.so 进行重定位,通常被符号链接到 /lib64/ld-linux-x86-64.so 中。动态链接库在编译时隐式指定 ld.so 的搜索路径,并写入 ELF Header 的 INTERP 字段中。从其他发行版直接拷贝已编译的 .so 文件可能会引发 ld.so 搜索路径不正确的问题。相似的,在版本依赖高度耦合的发行版中(如 ArchLinux),版本相差过大也会引发 ld.so 的运行失败。

本地同版本编译后通常不会出现问题。如果有直接拷贝已编译版本的需要,可以对比 interpreter 确定是否符合要求,但是不保证不会失败。

上面的例子中两个 libc 是这样的:

$ file /lib/x86_64-linux-gnu/libc-2.23.so
/lib/x86_64-linux-gnu/libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=088a6e00a1814622219f346b41e775b8dd46c518, for GNU/Linux 2.6.32, stripped
$ file ~/libc.so.6
/home/firmy/libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=088a6e00a1814622219f346b41e775b8dd46c518, for GNU/Linux 2.6.32, stripped

都是 interpreter /lib64/ld-linux-x86-64.so.2,所以可以替换。

而下面的例子是在 Arch Linux 上使用一个 Ubuntu 的 libc,就会出错:

$ ldd /bin/true
        linux-vdso.so.1 (0x00007ffc969df000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007f7ddde17000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f7dde3d7000)
$ LD_PRELOAD=~/libc.so.6 ldd /bin/true
Illegal instruction (core dumped)

一个在 interpreter /usr/lib/ld-linux-x86-64.so.2,而另一个在 interpreter /lib64/ld-linux-x86-64.so.2

environ

libc 中定义的全局变量 environ 指向环境变量表。而环境变量表存在于栈上,所以通过 environ 指针的值就可以泄露出栈地址。

gdb-peda$ vmmap libc
Start              End                Perm      Name
0x00007ffff7a1c000 0x00007ffff7bcf000 r-xp      /usr/lib/libc-2.27.so
0x00007ffff7bcf000 0x00007ffff7dce000 ---p      /usr/lib/libc-2.27.so
0x00007ffff7dce000 0x00007ffff7dd2000 r--p      /usr/lib/libc-2.27.so
0x00007ffff7dd2000 0x00007ffff7dd4000 rw-p      /usr/lib/libc-2.27.so
gdb-peda$ vmmap stack
Start              End                Perm      Name
0x00007ffffffde000 0x00007ffffffff000 rw-p      [stack]
gdb-peda$ shell nm -D /usr/lib/libc-2.27.so | grep environ
00000000003b8ee0 V environ
00000000003b8ee0 V _environ
00000000003b8ee0 B __environ
gdb-peda$ x/gx 0x00007ffff7a1c000 + 0x00000000003b8ee0
0x7ffff7dd4ee0 <environ>:       0x00007fffffffde48
gdb-peda$ x/5gx 0x00007fffffffde48
0x7fffffffde48: 0x00007fffffffe1da      0x00007fffffffe1e9
0x7fffffffde58: 0x00007fffffffe1fd      0x00007fffffffe233
0x7fffffffde68: 0x00007fffffffe25f
gdb-peda$ x/5s 0x00007fffffffe1da
0x7fffffffe1da: "COLORFGBG=15;0"
0x7fffffffe1e9: "COLORTERM=truecolor"
0x7fffffffe1fd: "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
0x7fffffffe233: "DESKTOP_SESSION=/usr/share/xsessions/plasma"
0x7fffffffe25f: "DISPLAY=:0"

procfs

procfs 文件系统是 Linux 内核提供的虚拟文件系统,为访问系统内核数据的操作提供接口。之所以说是虚拟文件系统,是因为它不占用存储空间,而只是占用了内存。用户可以通过 procfs 查看有关系统硬件及当前正在运行进程的信息,甚至可以通过修改其中的某些内容来改变内核的运行状态。

/proc/cmdline

在启动时传递给内核的相关参数信息,通常由 lilo 或 grub 等启动管理工具提供:

$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-4.14-x86_64 root=UUID=8e79a67d-af1b-4203-8c1c-3b670f0ec052 rw quiet resume=UUID=a220ecb1-7fde-4032-87bf-413057e9c06f

/proc/cpuinfo

记录 CPU 相关的信息:

$ cat /proc/cpuinfo
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 60
model name      : Intel(R) Core(TM) i5-4210H CPU @ 2.90GHz
stepping        : 3
microcode       : 0x24
cpu MHz         : 1511.087
cache size      : 3072 KB
physical id     : 0
siblings        : 4
core id         : 0
cpu cores       : 2
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 13
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm cpuid_fault epb invpcid_single pti ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
bugs            : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass
bogomips        : 5788.66
clflush size    : 64
cache_alignment : 64
address sizes   : 39 bits physical, 48 bits virtual
power management:
...

/proc/crypto

已安装的内核所使用的密码算法及算法的详细信息:

$ cat /proc/crypto
name         : ccm(aes)
driver       : ccm_base(ctr(aes-aesni),cbcmac(aes-aesni))
module       : ccm
priority     : 300
refcnt       : 2
selftest     : passed
internal     : no
type         : aead
async        : no
blocksize    : 1
ivsize       : 16
maxauthsize  : 16
geniv        : <none>
...

/proc/devices

已加载的所有块设备和字符设备的信息,包含主设备号和设备组(与主设备号对应的设备类型)名:

$ cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
...

/proc/interrupts

X86/X86_64 系统上每个 IRQ 相关的中断号列表,多路处理器平台上每个 CPU 对于每个 I/O 设备均有自己的中断号:

$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
  0:         15          0          0          0  IR-IO-APIC   2-edge      timer
  1:      46235       1277        325        156  IR-IO-APIC   1-edge      i8042
  8:          0          1          0          0  IR-IO-APIC   8-edge      rtc0
...
NMI:          0          0          0          0   Non-maskable interrupts
LOC:    7363806    5569019    6138317    5442200   Local timer interrupts
SPU:          0          0          0          0   Spurious interrupts
...

/proc/kcore

系统使用的物理内存,以 ELF 核心文件(core file)格式存储:

$ sudo file /proc/kcore
/proc/kcore: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from 'BOOT_IMAGE=/boot/vmlinuz-4.14-x86_64 root=UUID=8e79a67d-af1b-4203-8c1c-3b670f0e'

/proc/meminfo

系统中关于当前内存的利用状况等的信息:

$ cat /proc/meminfo
MemTotal:       12226252 kB
MemFree:         4909444 kB
MemAvailable:    8776048 kB
Buffers:          288236 kB
Cached:          3953616 kB
...

/proc/mounts

每个进程自身挂载名称空间中的所有挂载点列表文件的符号链接:

$ cat /proc/mounts
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
sys /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
dev /dev devtmpfs rw,nosuid,relatime,size=6106264k,nr_inodes=1526566,mode=755 0 0
...

/proc/modules

当前装入内核的所有模块名称列表,可以由 lsmod 命令使用。其中第一列表示模块名,第二列表示此模块占用内存空间大小,第三列表示此模块有多少实例被装入,第四列表示此模块依赖于其它哪些模块,第五列表示此模块的装载状态:Live(已经装入)、Loading(正在装入)和 Unloading(正在卸载),第六列表示此模块在内核内存(kernel memory)中的偏移量:

$ cat /proc/modules
fuse 118784 3 - Live 0xffffffffc0d9b000
ccm 20480 3 - Live 0xffffffffc0d95000
rfcomm 86016 4 - Live 0xffffffffc0d7f000
bnep 24576 2 - Live 0xffffffffc0d78000
...

/proc/slabinfo

保存着监视系统中所有活动的 slab 缓存的信息:

$ sudo cat /proc/slabinfo
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
fuse_request           0     20    400   20    2 : tunables    0    0    0 : slabdata      1      1      0
fuse_inode             1     39    832   39    8 : tunables    0    0    0 : slabdata      1      1      0
drm_i915_gem_request    765   1036    576   28    4 : tunables    0    0    0 : slabdata     37     37      0
...

/proc/[pid]

在 /proc 文件系统下,还有一些以数字命名的目录,这些数字是进程的 PID 号,而这些目录是进程目录。目录下的所有文件如下,然后会介绍几个比较重要的:

$ cat - &
[1] 1060
$ ls /proc/1060/
attr        comm             fd         maps        ns             personality  smaps         syscall
autogroup   coredump_filter  fdinfo     mem         numa_maps      projid_map   smaps_rollup  task
auxv        cpuset           gid_map    mountinfo   oom_adj        root         stack         timers
cgroup      cwd              io         mounts      oom_score      sched        stat          timerslack_ns
clear_refs  environ          limits     mountstats  oom_score_adj  schedstat    statm         uid_map
cmdline     exe              map_files  net         pagemap        setgroups    status        wchan

/proc/[pid]/cmdline

启动当前进程的完整命令:

$ cat /proc/1060/cmdline
cat-

/proc/[pid]/exe

指向启动当前进程的可执行文件的符号链接:

$ file /proc/1060/exe
/proc/1060/exe: symbolic link to /usr/bin/cat

/proc/[pid]/root

当前进程运行根目录的符号链接:

$ file /proc/1060/root
/proc/1060/root: symbolic link to /

/proc/[pid]/mem

当前进程所占用的内存空间,由open、read和lseek等系统调用使用,不能被用户读取。但可通过下面的 /proc/[pid]/maps 查看。

/proc/[pid]/maps

这个文件大概是最常用的,用于显示进程的内存区域映射信息:

$ cat /proc/1060/maps
56271b3a5000-56271b3ad000 r-xp 00000000 08:01 24904069                   /usr/bin/cat
56271b5ac000-56271b5ad000 r--p 00007000 08:01 24904069                   /usr/bin/cat
56271b5ad000-56271b5ae000 rw-p 00008000 08:01 24904069                   /usr/bin/cat
56271b864000-56271b885000 rw-p 00000000 00:00 0                          [heap]
7fefb66cd000-7fefb6a1e000 r--p 00000000 08:01 24912207                   /usr/lib/locale/locale-archive
7fefb6a1e000-7fefb6bd1000 r-xp 00000000 08:01 24905238                   /usr/lib/libc-2.27.so
7fefb6bd1000-7fefb6dd0000 ---p 001b3000 08:01 24905238                   /usr/lib/libc-2.27.so
7fefb6dd0000-7fefb6dd4000 r--p 001b2000 08:01 24905238                   /usr/lib/libc-2.27.so
7fefb6dd4000-7fefb6dd6000 rw-p 001b6000 08:01 24905238                   /usr/lib/libc-2.27.so
7fefb6dd6000-7fefb6dda000 rw-p 00000000 00:00 0
7fefb6dda000-7fefb6dff000 r-xp 00000000 08:01 24905239                   /usr/lib/ld-2.27.so
7fefb6fbd000-7fefb6fbf000 rw-p 00000000 00:00 0
7fefb6fdc000-7fefb6ffe000 rw-p 00000000 00:00 0
7fefb6ffe000-7fefb6fff000 r--p 00024000 08:01 24905239                   /usr/lib/ld-2.27.so
7fefb6fff000-7fefb7000000 rw-p 00025000 08:01 24905239                   /usr/lib/ld-2.27.so
7fefb7000000-7fefb7001000 rw-p 00000000 00:00 0
7ffde5659000-7ffde567a000 rw-p 00000000 00:00 0                          [stack]
7ffde5748000-7ffde574b000 r--p 00000000 00:00 0                          [vvar]
7ffde574b000-7ffde574d000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

/proc/[pid]/stack

这个文件表示当前进程的内核调用栈信息,只有在内核编译启用 CONFIG_STACKTRACE 选项,才会生成该文件:

$ sudo cat /proc/1060/stack
[<ffffffff8e08fa2e>] do_signal_stop+0xae/0x1f0
[<ffffffff8e090ec1>] get_signal+0x191/0x580
[<ffffffff8e02ae56>] do_signal+0x36/0x610
[<ffffffff8e003669>] exit_to_usermode_loop+0x69/0xa0
[<ffffffff8e0039d1>] do_syscall_64+0xf1/0x100
[<ffffffff8e800081>] entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[<ffffffffffffffff>] 0xffffffffffffffff

/proc/[pid]/auxv

该文件包含了传递给进程的解释器信息,即 auxv(AUXiliary Vector),每一项都是由一个 unsigned long 长度的 ID 加上一个 unsigned long 长度的值构成:

$ xxd -e -g8 /proc/1060/auxv
00000000: 0000000000000021 00007ffde574b000  !.........t.....
00000010: 0000000000000010 00000000bfebfbff  ................
00000020: 0000000000000006 0000000000001000  ................
00000030: 0000000000000011 0000000000000064  ........d.......
00000040: 0000000000000003 000056271b3a5040  ........@P:.'V..
00000050: 0000000000000004 0000000000000038  ........8.......
00000060: 0000000000000005 0000000000000009  ................
00000070: 0000000000000007 00007fefb6dda000  ................
00000080: 0000000000000008 0000000000000000  ................
00000090: 0000000000000009 000056271b3a7260  ........`r:.'V..
000000a0: 000000000000000b 00000000000003e8  ................
000000b0: 000000000000000c 00000000000003e8  ................
000000c0: 000000000000000d 00000000000003e8  ................
000000d0: 000000000000000e 00000000000003e8  ................
000000e0: 0000000000000017 0000000000000000  ................
000000f0: 0000000000000019 00007ffde5678349  ........I.g.....
00000100: 000000000000001a 0000000000000000  ................
00000110: 000000000000001f 00007ffde5679fef  ..........g.....
00000120: 000000000000000f 00007ffde5678359  ........Y.g.....
00000130: 0000000000000000 0000000000000000  ................

每个值具体是做什么的,可以用下面的办法显示出来,对比看一看,更详细的可以查看 /usr/include/elf.hman ld.so

$ LD_SHOW_AUXV=1 cat -
AT_SYSINFO_EHDR: 0x7ffd16be5000
AT_HWCAP:        bfebfbff
AT_PAGESZ:       4096
AT_CLKTCK:       100
AT_PHDR:         0x55eb4c59a040
AT_PHENT:        56
AT_PHNUM:        9
AT_BASE:         0x7f61506e8000
AT_FLAGS:        0x0
AT_ENTRY:        0x55eb4c59c260
AT_UID:          1000
AT_EUID:         1000
AT_GID:          1000
AT_EGID:         1000
AT_SECURE:       0
AT_RANDOM:       0x7ffd16bd0ce9
AT_HWCAP2:       0x0
AT_EXECFN:       /bin/cat
AT_PLATFORM:     x86_64

值得一提的是,AT_SYSINFO_EHDR 所对应的值是一个叫做的 VDSO(Virtual Dynamic Shared Object) 的地址。在 ret2vdso 漏洞利用方法中会用到(参考章节6.1.6)。

/proc/[pid]/environ

该文件包含了进程的环境变量:

$ strings /proc/1060/environ
GS_LIB=/home/firmy/.fonts
KDE_FULL_SESSION=true
VIRTUALENVWRAPPER_WORKON_CD=1
VIRTUALENVWRAPPER_HOOK_DIR=/home/firmy/.virtualenvs
LANG=zh_CN.UTF-8
...

/proc/[pid]/fd

该文件包含了进程打开文件的情况:

$ ls -al /proc/1060/fd
total 0
dr-x------ 2 firmy firmy  0 6月   7 23:37 .
dr-xr-xr-x 9 firmy firmy  0 6月   7 23:37 ..
lrwx------ 1 firmy firmy 64 6月   7 23:44 0 -> /dev/pts/3
lrwx------ 1 firmy firmy 64 6月   7 23:44 1 -> /dev/pts/3
lrwx------ 1 firmy firmy 64 6月   7 23:44 2 -> /dev/pts/3

/proc/[pid]/status

该文件包含了进程的状态信息:

$ cat /proc/1060/status
Name:   cat
Umask:  0022
State:  T (stopped)
Tgid:   1060
Ngid:   0
Pid:    1060
PPid:   1035
TracerPid:      0
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
FDSize: 256
Groups: 3 7 10 56 90 91 93 95 96 98 1000
...

/proc/[pid]/task

一个目录,包含当前进程的每一个线程的相关信息,每个线程的信息分别放在一个由线程号(tid)命名的目录中:

$ ls /proc/1060/task/
1060
$ ls /proc/1060/task/1060/
attr      clear_refs  cwd      fdinfo   maps       net        oom_score      projid_map  setgroups     stat     uid_map
auxv      cmdline     environ  gid_map  mem        ns         oom_score_adj  root        smaps         statm    wchan
cgroup    comm        exe      io       mountinfo  numa_maps  pagemap        sched       smaps_rollup  status
children  cpuset      fd       limits   mounts     oom_adj    personality    schedstat   stack         syscall

/proc/[pid]/syscall

该文件包含了进程正在执行的系统调用:

$ sudo cat /proc/1060/syscall
0 0x0 0x7fefb6fdd000 0x20000 0x22 0xffffffff 0x0 0x7ffde5677d48 0x7fefb6b07901

第一个值是系统调用号,后面跟着是六个参数,最后两个值分别是堆栈指针和指令计数器的值。

参考资料

1.5.1 C 语言基础

从源代码到可执行文件

我们以经典著作《The C Programming Language》中的第一个程序 “Hello World” 为例,讲解 Linux 下 GCC 的编译过程。

#include <stdio.h>
main()
{
    printf("hello, world\n");
}

$gcc hello.c
$./a.out
hello world

以上过程可分为4个步骤:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。

img

预编译

gcc -E hello.c -o hello.i
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
......
extern int printf (const char *__restrict __format, ...);
......
main() {
 printf("hello, world\n");
}

预编译过程主要处理源代码中以 “#” 开始的预编译指令:

  • 将所有的 “#define” 删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,如 “#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
  • 处理 “#include” 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,该过程递归执行。
  • 删除所有注释。
  • 添加行号和文件名标号。
  • 保留所有的 #pragma 编译器指令。

编译

gcc -S hello.c -o hello.s
        .file   "hello.c"
        .section        .rodata
.LC0:
        .string "hello, world"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        leaq    .LC0(%rip), %rdi
        call    puts@PLT
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (GNU) 7.2.0"
        .section        .note.GNU-stack,"",@progbits

编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。

汇编

$ gcc -c hello.s -o hello.o
或者
$gcc -c hello.c -o hello.o
$ objdump -sd hello.o
hello.o:     file format elf64-x86-64
Contents of section .text:
 0000 554889e5 488d3d00 000000e8 00000000  UH..H.=.........
 0010 b8000000 005dc3                      .....].
Contents of section .rodata:
 0000 68656c6c 6f2c2077 6f726c64 00        hello, world.
Contents of section .comment:
 0000 00474343 3a202847 4e552920 372e322e  .GCC: (GNU) 7.2.
 0010 3000                                 0.
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 17000000 00410e10 8602430d  .........A....C.
 0030 06520c07 08000000                    .R......
Disassembly of section .text:
0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # b <main+0xb>
   b:   e8 00 00 00 00          callq  10 <main+0x10>
  10:   b8 00 00 00 00          mov    $0x0,%eax
  15:   5d                      pop    %rbp
  16:   c3                      retq

汇编器将汇编代码转变成机器可以执行的指令。

链接

gcc hello.o -o hello
$ objdump -d -j .text hello
......
000000000000064a <main>:
 64a:   55                      push   %rbp
 64b:   48 89 e5                mov    %rsp,%rbp
 64e:   48 8d 3d 9f 00 00 00    lea    0x9f(%rip),%rdi        # 6f4 <_IO_stdin_used+0x4>
 655:   e8 d6 fe ff ff          callq  530 <puts@plt>
 65a:   b8 00 00 00 00          mov    $0x0,%eax
 65f:   5d                      pop    %rbp
 660:   c3                      retq
 661:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
 668:   00 00 00
 66b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
......

目标文件需要链接一大堆文件才能得到最终的可执行文件(上面只展示了链接后的 main 函数,可以和 hello.o 中的 main 函数作对比)。链接过程主要包括地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定向(Relocation)等。

gcc 技巧

通常在编译后只会生成一个可执行文件,而中间过程生成的 .i.s.o 文件都不会被保存。我们可以使用参数 -save-temps 永久保存这些临时的中间文件。

$ gcc -save-temps hello.c
$ ls
a.out hello.c  hello.i  hello.o  hello.s

这里要注意的是,gcc 默认使用动态链接,所以这里生成的 a.out 实际上是共享目标文件。

$ file a.out
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=533aa4ca46d513b1276d14657ec41298cafd98b1, not stripped

使用参数 --verbose 可以输出 gcc 详细的工作流程。

gcc hello.c -static --verbose

东西很多,我们主要关注下面几条信息:

$ /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/cc1 -quiet -v hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -o /tmp/ccj1jUMo.s
as -v --64 -o /tmp/ccAmXrfa.o /tmp/ccj1jUMo.s
/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/collect2 -plugin /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/lto-wrapper -plugin-opt=-fresolution=/tmp/cc1l5oJV.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --build-id --hash-style=gnu -m elf_x86_64 -static /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib/crt1.o /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib/crti.o /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/crtbeginT.o -L/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0 -L/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../.. /tmp/ccAmXrfa.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/crtend.o /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib/crtn.o

三条指令分别是 cc1ascollect2,cc1 是 gcc 的编译器,将 .c 文件编译为 .s 文件,as 是汇编器命令,将 .s 文件汇编成 .o 文件,collect2 是链接器命令,它是对命令 ld 的封装。静态链接时,gcc 将 C 语言运行时库的 5 个重要目标文件 crt1.ocrti.ocrtbeginT.ocrtend.ocrtn.o-lgcc-lgcc_eh-lc 表示的 3 个静态库链接到可执行文件中。

更多的内容我们会在 1.5.3 中专门对 ELF 文件进行讲解。

C 语言标准库

C 运行库(CRT)是一套庞大的代码库,以支撑程序能够正常地运行。其中 C 语言标准库占据了最主要地位。

常用的标准库文件头:

  • 标准输入输出(stdio.h)
  • 字符操作(ctype.h)
  • 字符串操作(string.h)
  • 数学函数(math.h)
  • 实用程序库(stdlib.h)
  • 时间/日期(time.h)
  • 断言(assert.h)
  • 各种类型上的常数(limits.h & float.h)
  • 变长参数(stdarg.h)
  • 非局部跳转(setjmp.h)

glibc 即 GNU C Library,是为 GNU 操作系统开发的一个 C 标准库。glibc 主要由两部分组成,一部分是头文件,位于 /usr/include;另一部分是库的二进制文件。二进制文件部分主要是 C 语言标准库,有动态和静态两个版本,动态版本位于 /lib/libc.so.6,静态版本位于 /usr/lib/libc.a

在漏洞利用的过程中,通常我们通过计算目标函数地址相对于已知函数地址在同一个 libc 中的偏移,来获得目标函数的虚拟地址,这时我们需要让本地的 libc 版本和远程的 libc 版本相同,可以先泄露几个函数的地址,然后在 libcdb.com 中进行搜索来得到。

整数表示

默认情况下,C 语言中的数字是有符号数,下面我们声明一个有符号整数和无符号整数:

int var1 = 0;unsigned int var2 = 0;
  • 有符号整数
    • 可以表示为正数或负数
    • int 的范围:-2,147,483,648 ~ 2,147,483,647
  • 无符号整数
    • 只能表示为零或正数
    • unsigned int 的范围:0 ~ 4,294,967,295

signed 或者 unsigned 取决于整数类型是否可以携带标志 +/-

  • Signed
    • int
    • signed int
    • long
  • Unsigned
    • unit
    • unsigned int
    • unsigned long

signed int 中,二进制最高位被称作符号位,符号位被设置为 1 时,表示值为负,当设置为 0 时,值为非负:

  • 0x7FFFFFFF = 2147493647
    • 01111111111111111111111111111111
  • 0x80000000 = -2147483647
    • 10000000000000000000000000000000
  • 0xFFFFFFFF = -1
    • 11111111111111111111111111111111

二进制补码以一种适合于二进制加法器的方式来表示负数,当一个二进制补码形式表示的负数和与它的绝对值相等的正数相加时,结果为 0。首先以二进制方式写出正数,然后对所有位取反,最后加 1 就可以得到该数的二进制补码:

eg: 0x00123456
  = 1193046
  = 00000000000100100011010001010110
 ~= 11111111111011011100101110101001
 += 11111111111011011100101110101010
  = -1193046 (0xFFEDCBAA)

编译器需要根据变量类型信息编译成相应的指令:

  • 有符号指令
    • IDIV:带符号除法指令
    • IMUL:带符号乘法指令
    • SAL:算术左移指令(保留符号)
    • SAR:右移右移指令(保留符号)
    • MOVSX:带符号扩展传送指令
    • JL:当小于时跳转指令
    • JLE:当小于或等于时跳转指令
    • JG:当大于时跳转指令
    • JGE:当大于或等于时跳转指令
  • 无符号指令
    • DIV:除法指令
    • MUL:乘法指令
    • SHL:逻辑左移指令
    • SHR:逻辑右移指令
    • MOVZX:无符号扩展传送指令
    • JB:当小于时跳转指令
    • JBE:当小于或等于时跳转指令
    • JA:当大于时跳转指令
    • JAE:当大于或等于时跳转指令

32 位机器上的整型数据类型,不同的系统可能会有不同:

C 数据类型最小值最大值最小大小
char-1281278 bits
short-32 76832 76716 bits
int-2 147 483 6482 147 483 64716 bits
long-2 147 483 6482 147 483 64732 bits
long long-9 223 372 036 854 775 8089 223 372 036 854 775 80764 bits

固定大小的数据类型:

  • int [# of bits]_t
    
    • int8_t, int16_t, int32_t
  • uint[# of bits]_t

    • uint8_t, uint16_t, uint32_t
  • 有符号整数

    • img
  • 无符号整数

更多信息在 stdint.hlimits.h 中:

man stdint.h
cat /usr/include/stdint.h
man limits.h
cat /usr/include/limits.h

了解整数的符号和大小是很有用的,在后面的相关章节中我们会介绍整数溢出的内容。

格式化输出函数

C 标准中定义了下面的格式化输出函数(参考 man 3 printf):

#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vdprintf(int fd, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
  • fprintf() 按照格式字符串的内容将输出写入流中。三个参数为流、格式字符串和变参列表。
  • printf() 等同于 fprintf(),但是它假定输出流为 stdout
  • sprintf() 等同于 fprintf(),但是输出不是写入流而是写入数组。在写入的字符串末尾必须添加一个空字符。
  • snprintf() 等同于 sprintf(),但是它指定了可写入字符的最大值 size。当 size 大于零时,输出字符超过第 size-1 的部分会被舍弃而不会写入数组中,在写入数组的字符串末尾会添加一个空字符。
  • dprintf() 等同于 fprintf(),但是它输出不是流而是一个文件描述符 fd
  • vfprintf()vprintf()vsprintf()vsnprintf()vdprintf() 分别与上面的函数对应,只是它们将变参列表换成了 va_list 类型的参数。

格式字符串

格式字符串是由普通字符(ordinary character)(包括 %)和转换规则(conversion specification)构成的字符序列。普通字符被原封不动地复制到输出流中。转换规则根据与实参对应的转换指示符对其进行转换,然后将结果写入输出流中。

一个转换规则有可选部分和必需部分组成:

%[ 参数 ][ 标志 ][ 宽度 ][ .精度 ][ 长度 ] 转换指示符
  • (必需)转换指示符
字符描述
d, i有符号十进制数值 int。’%d‘ 与 ‘%i‘ 对于输出是同义;但对于 scanf() 输入二者不同,其中 %i 在输入值有前缀 0x0 时,分别表示 16 进制或 8 进制的值。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空
u十进制 unsigned int。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空
f, Fdouble 型输出 10 进制定点表示。’f‘ 与 ‘F‘ 差异是表示无穷与 NaN 时,’f‘ 输出 ‘inf‘, ‘infinity‘ 与 ‘nan‘;’F‘ 输出 ‘INF‘, ‘INFINITY‘ 与 ‘NAN‘。小数点后的数字位数等于精度,最后一位数字四舍五入。精度默认为 6。如果精度为 0 且没有 # 标记,则不出现小数点。小数点左侧至少一位数字
e, Edouble 值,输出形式为 10 进制的([-]d.ddd e[+/-]ddd). E 版本使用的指数符号为 E(而不是e)。指数部分至少包含 2 位数字,如果值为 0,则指数部分为 00。Windows 系统,指数部分至少为 3 位数字,例如 1.5e002,也可用 Microsoft 版的运行时函数 _set_output_format 修改。小数点前存在 1 位数字。小数点后的数字位数等于精度。精度默认为 6。如果精度为 0 且没有 # 标记,则不出现小数点
g, Gdouble 型数值,精度定义为全部有效数字位数。当指数部分在闭区间 [-4,精度] 内,输出为定点形式;否则输出为指数浮点形式。’g‘ 使用小写字母,’G‘ 使用大写字母。小数点右侧的尾数 0 不被显示;显示小数点仅当输出的小数部分不为 0
x, X16 进制 unsigned int。’x‘ 使用小写字母;’X‘ 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空
o8 进制 unsigned int。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空
s如果没有用 l 标志,输出 null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 l 标志,则对应函数参数指向 wchar_t 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 wcrtomb 函数
c如果没有用 l 标志,把 int 参数转为 unsigned char 型输出;如果用了 l 标志,把 wint_t 参数转为包含两个元素的 wchart_t 数组,其中第一个元素包含要输出的字符,第二个元素为 null 宽字符
pvoid * 型,输出对应变量的值。printf("%p", a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址
a, Adouble 型的 16 进制表示,”[−]0xh.hhhh p±d”。其中指数部分为 10 进制表示的形式。例如:1025.010 输出为 0x1.004000p+10。’a‘ 使用小写字母,’A‘ 使用大写字母
n不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量
%%‘ 字面值,不接受任何除了 参数 以外的部分
  • (可选)参数
字符描述
n$n 是用这个格式说明符显示第几个参数;这使得参数可以输出多次,使用多个格式说明符,以不同的顺序输出。如果任意一个占位符使用了 参数,则其他所有占位符必须也使用 参数。例:printf("%2$d %2$#x; %1$d %1$#x",16,17) 产生 “17 0x11; 16 0x10
  • (可选)标志
字符描述
+总是表示有符号数值的 ‘+‘ 或 ‘-‘ 号,缺省情况是忽略正数的符号。仅适用于数值类型
空格使得有符号数的输出如果没有正负号或者输出 0 个字符,则前缀 1 个空格。如果空格与 ‘+‘ 同时出现,则空格说明符被忽略
-左对齐。缺省情况是右对齐
#对于 ‘g‘ 与 ‘G‘,不删除尾部 0 以表示精度。对于 ‘f‘, ‘F‘, ‘e‘, ‘E‘, ‘g‘, ‘G‘, 总是输出小数点。对于 ‘o‘, ‘x‘, ‘X‘, 在非 0 数值前分别输出前缀 0, 0x0X表示数制
0如果 宽度 选项前缀为 0,则在左侧用 0 填充直至达到宽度要求。例如 printf("%2d", 3) 输出 “3“,而 printf("%02d", 3) 输出 “03“。如果 0- 均出现,则 0 被忽略,即左对齐依然用空格填充
  • (可选)宽度

是一个用来指定输出字符的最小个数的十进制非负整数。如果实际位数多于定义的宽度,则按实际位数输出;如果实际位数少于定义的宽度则补以空格或 0。

  • (可选)精度

精度是用来指示打印字符个数、小数位数或者有效数字个数的非负十进制整数。对于 diuxo 的整型数值,是指最小数字位数,不足的位要在左侧补 0,如果超过也不截断,缺省值为 1。对于 a, A, e, E, f, F 的浮点数值,是指小数点右边显示的数字位数,必要时四舍五入;缺省值为 6。对于 g, G 的浮点数值,是指有效数字的最大位数。对于 s 的字符串类型,是指输出的字节的上限,超出限制的其它字符将被截断。如果域宽为 *,则由对应的函数参数的值为当前域宽。如果仅给出了小数点,则域宽为 0。

  • (可选)长度
字符描述
hh对于整数类型,printf 期待一个从 char 提升的 int 整型参数
h对于整数类型,printf 期待一个从 short 提升的 int 整型参数
l对于整数类型,printf 期待一个 long 整型参数。对于浮点类型,printf 期待一个 double 整型参数。对于字符串 s 类型,printf 期待一个 wchar_t 指针参数。对于字符 c 类型,printf 期待一个 wint_t 型的参数
ll对于整数类型,printf 期待一个 long long 整型参数。Microsoft 也可以使用 I64
L对于浮点类型,printf 期待一个 long double 整型参数
z对于整数类型,printf 期待一个 size_t 整型参数
j对于整数类型,printf 期待一个 intmax_t 整型参数
t对于整数类型,printf 期待一个 ptrdiff_t 整型参数

例子

printf("Hello %%");           // "Hello %"
printf("Hello World!");       // "Hello World!"
printf("Number: %d", 123);    // "Number: 123"
printf("%s %s", "Format", "Strings");   // "Format Strings"
printf("%12c", 'A');          // "           A"
printf("%16s", "Hello");      // "          Hello!"
int n;
printf("%12c%n", 'A', &n);    // n = 12
printf("%16s%n", "Hello!", &n); // n = 16
printf("%2$s %1$s", "Format", "Strings"); // "Strings Format"
printf("%42c%1$n", &n);       // 首先输出41个空格,然后输出 n 的低八位地址作为一个字符

这里我们对格式化输出函数和格式字符串有了一个详细的认识,后面的章节中我们会介绍格式化字符串漏洞的内容。

  • 汇编语言
    • 3.3 X86 汇编基础
      • [3.3.2 寄存器 Registers](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.3.2 寄存器 Registers)
      • 3.3.3 内存和寻址模式 Memory and Addressing Modes
        • [3.3.3.1 声明静态数据区域](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.3.3.1 声明静态数据区域)
        • [3.3.3.2 内存寻址](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.3.3.2 内存寻址)
        • [3.3.3.3 操作后缀](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.3.3.3 操作后缀)
      • 3.3.4 指令 Instructions
        • [3.3.4.1 数据移动指令](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.3.4.1 数据移动指令)
        • [3.3.4.2 逻辑运算指令](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.3.4.2 逻辑运算指令)
        • [3.3.4.3 流程控制指令](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.3.4.3 流程控制指令)
      • 3.3.5 调用约定 Calling Convention
        • [3.3.5.1 调用者约定 Caller Rules](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.3.5.1 调用者约定 Caller Rules)
        • [3.3.5.2 被调用者约定 Callee Rules](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.3.5.2 被调用者约定 Callee Rules)
    • 3.4 x64 汇编基础
      • [3.4.1 导语](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.4.1 导语)
      • [3.4.2 寄存器 Registers](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.4.2 寄存器 Registers)
      • [3.4.3 寻址模式 Addressing modes](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.4.3 寻址模式 Addressing modes)
      • 3.4.4 通用指令 Common instructions
      • [3.4.5 汇编和 gdb](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.4.5 汇编和 gdb)
    • 3.5 ARM汇编基础
      • [3.5.1 引言](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.5.1 引言)
      • [3.5.2 ARM 的 GNU 汇编程序指令表](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.5.2 ARM 的 GNU 汇编程序指令表)
      • [3.5.3 寄存器名称](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.5.3 寄存器名称)
      • [3.5.4 汇编程序特殊字符/语法](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.5.4 汇编程序特殊字符/语法)
      • [3.5.5 arm程序调用标准](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.5.5 arm程序调用标准)
      • [3.5.6 寻址模式](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.5.6 寻址模式)
      • [3.5.7 机器相关指令](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#3.5.7 机器相关指令)
    • 3.6 MIPS汇编基础
      • 数据类型和常量
      • 寄存器
      • 程序结构
        • 数据声明
        • 代码
        • 注释
        • 变量声明
        • [读取/写入 ( Load/Store )指令](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#读取/写入 ( Load/Store )指令)
        • 间接和立即寻址
        • 算术指令
        • 流程控制
        • [系统调用和 I / O( 针对 SPIM 模拟器 )](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#系统调用和 I / O( 针对 SPIM 模拟器 ))
        • [补充 : MIPS 指令格式](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#补充 : MIPS 指令格式)
        • [补充 : MIPS 常用指令集](https://www.bookstack.cn/read/CTF-All-In-One/doc-1.5.2_assembly.md#补充 : MIPS 常用指令集)
      • 参考资料

汇编语言

3.3 X86 汇编基础


3.3.2 寄存器 Registers

现代 ( 386及以上的机器 )x86 处理器有 8 个 32 位通用寄存器, 如图 1 所示.x86-registers.png

这些寄存器的名字都是有点历史的, 例如 EAX 过去被称为 累加器, 因为它被用来作很多算术运算, 还有 ECX 被称为 计数器 , 因为它被用来保存循环的索引 ( 就是循环次数 ). 尽管大多是寄存器在现代指令集中已经失去了它们的特殊用途, 但是按照惯例, 其中有两个寄存器还是有它们的特殊用途 —-ESP 和 EBP.

对于 EAS, EBX, ECX 还有 EDX 寄存器, 它们可以被分段开来使用. 例如, 可以将 EAX 的最低的 2 位字节视为 16 位寄存器 ( AX ). 还可以将 AX 的最低位的 1 个字节看成 8 位寄存器来用 ( AL ), 当然 AX 的高位的 1 个字节也可以看成是一个 8 位寄存器 ( AH ). 这些名称有它们相对应的物理寄存器. 当两个字节大小的数据被放到 DX 的时候, 原本 DH, DLEDX 的数据会受到影响 ( 被覆盖之类的 ). 这些 “ 子寄存器 “ 主要来自于比较久远的 16 位版本指令集. 然而, 姜还是老的辣, 在处理小于 32 位的数据的时候, 比如 1 个字节的 ASCII 字符, 它们有时会很方便.

3.3.3 内存和寻址模式 Memory and Addressing Modes

3.3.3.1 声明静态数据区域

你可以用特殊的 x86 汇编指令在内存中声明静态数据区域 ( 类似于全局变量 ). .data指令用来声明数据. 根据这条指令, .byte, .short.long 可以分别用来声明 1 个字节, 2 个字节和 4 个字节的数据. 我们可以给它们打个标签, 用来引用创建的数据的地址. 标签在汇编语言中是非常有用的, 它们给内存地址命名, 然后编译器链接器 将其 “ 翻译 “ 成计算机理解的机器代码. 这个跟用名称来声明变量很类似, 但是它遵守一些较低级别的规则. 例如, 按顺序声明的位置将彼此相邻地存储在内存中. 这话也许有点绕, 就是按照顺序打的标签, 这些标签对应的数据也会按照顺序被放到内存中.

一些例子 :

.data
var :
       .byte 64 ;声明一个字节型变量 var, 其所对应的数据是64
       .byte 10 ;声明一个数据 10, 这个数据没有所谓的 " 标签 ", 它的内存地址就是 var+1.
x :
       .short 42 ;声明一个大小为 2 个字节的数据, 这个数据有个标签 " x "
y :
       .long 30000 ;声明一个大小为 4 个字节的数据, 这个数据标签是 " y ",  y 的值被初始化为 30000

与高级语言不同, 高级语言的数组可以具有多个维度并且可以通过索引来访问, x86 汇编语言的数组只是在内存中连续的” 单元格 “. 你只需要把数值列出来就可以声明一个数组, 比如下面的第一个例子. 对于一些字节型数组的特殊情况, 我们可以使用字符串. 如果要在大多数的内存填充 0, 你可以使用.zero指令.

例子 :

s :
       .long 1, 2, 3 ;声明 3 个大小为 4 字节的数据 1, 2, 3. 内存中 s+8 这个标签所对应的数据就是 3.
barr:
       .zero 10 ;从 barr 这个标签的位置开始, 声明 10 个字节的数据, 这些数据被初始化为 0.
str :
       .string "hello" ;从 str 这个标签的位置开始, 声明 6 个字节的数据, 即 hello 对应的 ASCII 值, 这最后还跟有一个 nul(0) 字节.

label_s

label_str

3.3.3.2 内存寻址

现代x86兼容处理器能够寻址高达 2^32 字节的内存 : 内存地址为 32 位宽. 在上面的示例中,我们使用标签来引用内存区域,这些标签实际上被 32 位数据的汇编程序替换,这些数据指定了内存中的地址. 除了支持通过标签(即常数值)引用存储区域之外,x86提供了一种灵活的计算和引用内存地址的方案 :最多可将两个32位寄存器和一个32位有符号常量相加,以计算存储器地址. 其中一个寄存器可以选择预先乘以 2, 4 或 8.

寻址模式可以和许多 x86 指令一起使用 ( 我们将在下一节对它们进行讲解 ). 这里我们用mov指令在寄存器和内存中移动数据当作例子. 这个指令有两个参数, 第一个是数据的来源, 第二个是数据的去向.

一些mov的例子 :

mov (%ebx), %eax ;从 EBX 中的内存地址加载 4 个字节的数据到 EAX, 就是把 EBX 中的内容当作标签, 这个标签在内存中对应的数据放到 EAX 中
;后面如果没有说明的话, (%ebx)就表示寄存器ebx中存储的内容
mov %ebx, var(,1) ; 将 EBX 中的 4 个字节大小的数据移动的内存中标签为 var 的地方去.( var 是一个 32 位常数).
mov (%esi, %ebx, 4), %edx ;将内存中标签为 ESI+4*EBX 所对应的 4 个字节大小的数据移动到 EDX中.

一些错误的例子:

mov (%ebx, %ecx, -1), %eax ;这个只能把寄存器中的值加上一遍.
mov %ebx,(%eax, %esi, %edi, 1) ;在地址计算中, 最多只能出现 2 个寄存器, 这里却有 3 个寄存器.
3.3.3.3 操作后缀

通常, 给定内存地址的数据类型可以从引用它的汇编指令推断出来. 例如, 在上面的指令中, 你可以从寄存器操作数的大小来推出其所占的内存大小. 当我们加载一个 32 位的寄存器的时候, 编译器就可以推断出我们用到的内存大小是 4 个字节宽. 当我们将 1 个字节宽的寄存器的值保存到内存中时, 编译器可以推断出我们想要在内存中弄个 1 字节大小的 “ 坑 “ 来保存我们的数据.

然而在某些情况下, 我们用到的内存中 “ 坑 “ 的大小是不明确的. 比如说这条指令 mov $2,(%ebx). 这条指令是否应该将 “ 2 “ 这个值移动到 EBX 中的值所代表的地址 “ 坑 “ 的单个字节中 ? 也许它表示的是将 32 位整数表示的 2 移动到从地址 EBX 开始的 4 字节. 既然这两个解释都有道理, 但计算机汇编程序必须明确哪个解释才是正确的, 计算机很单纯的, 要么是错的要么是对的. 前缀 b, w, 和 l 就是来解决这个问题的, 它们分别表示 1, 2 和 4 个字节的大小.

举几个例子 :

movb $2, (%ebx) ;将 2 移入到 ebx 中的值所表示的地址单元中.
movw $2, (%ebx) ;将 16 位整数 2 移动到 从 ebx 中的值所表示的地址单元 开始的 2 个字节中;这话有点绕, 所以我故意在里面加了点空格, 方便大家理解.
movl $2,(%ebx) ;将 32 位整数 2 移动到 从 ebx中的值表示的地址单元 开始的 4 个字节中.

3.3.4 指令 Instructions

机器指令通常分为 3 类 : 数据移动指令, 逻辑运算指令和流程控制指令. 在本节中, 我们将讲解每一种类型的 x86 指令以及它们的重要示例. 当然, 我们不可能把 x86 所有指令讲得特别详细, 毕竟篇幅和水平有限. 完整的指令列表, 请参阅 intel 的指令集参考手册.

我们将使用以下符号 :

<reg32 任意的 32 位寄存器 (%eax, %ebx, %ecx, %edx, %esi, %edi, %esp 或者 %eb)
<reg16 任意的 16 位寄存器 (%ax, %bx, %cx 或者 %dx)
<reg8 任意的 8 位寄存器 (%ah, %al, %bh, %bl, %ch, %cl, %dh, %dl)
<reg 任意的寄存器
<mem 一个内存地址, 例如 (%eax), 4+var, (%eax, %ebx, 1)
<con32 32 位常数
<con16 16 位常数
<con8 8 位常数
<con 任意 32位, 16 位或者 8 位常数

在汇编语言中, 用作立即操作数 的所有标签和数字常量 ( 即不在诸如3 (%eax, %ebx, 8)这样的地址计算中 ) 总是以美元符号 $ 为前缀. 需要的时候, 前缀 0x 表示十六进制数, 例如$ 0xABC. 如果没有前缀, 则默认该数字为十进制数.

3.3.4.1 数据移动指令
  • mov 移动

mov 指令将数据从它的第一个参数 ( 即寄存器中的内容, 内存单元中的内容, 或者一个常数值 ) 复制到它的第二个参数 ( 即寄存器或者内存单元 ). 当寄存器到寄存器之间的数据移动是可行的时候, 直接地从内存单元中将数据移动到另一内存单元中是不行的. 在这种需要在内存单元中传递数据的情况下, 它数据来源的那个内存单元必须首先把那个内存单元中的数据加载到一个寄存器中, 然后才可以通过这个寄存器来把数据移动到目标内存单元中.

  • 语法
mov <reg, <reg
mov <reg, <mem
mov <mem, <reg
mov <con, <reg
mov <con, <mem
  • 例子
mov %ebx, %eax ;将 EBX 中的值复制到 EAX 中
mov $5, var(,1) ;将数字 5 存到字节型内存单元 " var "

mov_1

  • push 入栈

push指令将它的参数移动到硬件支持的内存顶端. 特别地, push 首先将 ESP 中的值减少 4, 然后将它的参数移动到一个 32 位的地址单元 ( %esp ). ESP ( 栈指针 ) 会随着不断入栈从而持续递减, 即栈内存是从高地址单元到低地址单元增长.

  • 语法
push <reg32
push <mem
push <con32
  • 例子
push %eax ;将 EAX 送入栈
push var(,1) ;将 var 对应的 4 字节大小的数据送入栈中
  • pop 出栈

pop指令从硬件支持的栈内存顶端移除 4 字节的数据, 并把这个数据放到该指令指定的参数中 ( 即寄存器或者内存单元 ). 其首先将内存中 ( %esp ) 的 4 字节数据放到指定的寄存器或者内存单元中, 然后让 ESP + 4.

  • 语法
pop <reg32
pop <mem
  • 例子
pop %edi ;将栈顶的元素移除, 并放入到寄存器 EDI 中.
pop (%ebx) ;将栈顶的元素移除, 并放入从 EBX 开始的 4 个字节大小的内存单元中.

重点内容 : 栈栈是一种特殊的存储空间, 特殊在它的访问形式上, 它的访问形式就是最后进入这个空间的数据, 最先出去, 也就是 “先进后出, 后进先出”.

  • lea加载有效地址

lea指令将其第一个参数指定的内存单元 放入到 第二个参数指定的寄存器中. 注意, 该指令不加载内存单元中的内容, 只是计算有效地址并将其放入寄存器. 这对于获得指向存储器区域的指针或者执行简单的算术运算非常有用.

也许这里你会看得一头雾水, 不过你不必担心, 这里有更为通俗易懂的解释.汇编语言中 lea 指令和 mov 指令的区别 ?MOV 指令的功能是传送数据,例如 MOV AX,[1000H],作用是将 1000H 作为偏移地址,寻址找到内存单元,将该内存单元中的数据送至 AX;LEA 指令的功能是取偏移地址,例如 LEA AX,[1000H],作用是将源操作数 [1000H] 的偏移地址 1000H 送至 AX。理解时,可直接将[ ]去掉,等同于 MOV AX,1000H。再如:LEA BX,[AX],等同于 MOV BX,AXLEA BX,TABLE 等同于 MOV BX,OFFSET TABLE。但有时不能直接使用 MOV 代替:比如:LEA AX,[SI+6] 不能直接替换成:MOV AX,SI+6;但可替换为:MOV AX,SI``ADD AX,6两步完成。

参考链接

  • 语法
lea <mem, <reg32
  • 例子
lea (%ebx,%esi,8), %edi ;EBX+8*ESI 的值被移入到了 EDI
lea val(,1), %eax ;val 的值被移入到了 EAX
3.3.4.2 逻辑运算指令
  • add 整数相加

add 指令将两个参数相加, 然后将结果存放到第二个参数中. 注意, 参数可以是寄存器,但参数中最多只有一个内存单元. 这话有点绕, 我们直接看语法 :

  • 语法
add <reg, <reg
add <mem, <reg
add <reg, <mem
add <con, <reg
add <con, <mem
  • 例子
add $10, %eax ;EAX 中的值被设置为了 EAX+10.
addb $10, (%eax) ;往 EAX 中的值 所代表的内存单元地址 加上 1 个字节的数字 10.
  • sub 整数相减

sub指令将第二个参数的值与第一个相减, 就是后面那个减去前面那个, 然后把结果存储到第二个参数. 和add一样, 两个参数都可以是寄存器, 但两个参数中最多只能有一个是内存单元.

  • 语法
sub <reg, <reg
sub <mem, <reg
sub <con, <reg
sub <con, <mem
  • 例子
sub %ah, %al ;AL 被设置成 AL-AH
sub $216, %eax ;将 EAX 中的值减去 216
  • inc, dec 自增, 自减

inc 指令让它的参数加 1, dec 指令则是让它的参数减去 1.

  • 语法
inc <reg
inc <mem
dec <reg
dec <mem
  • 例子
dec %eax ;EAX 中的值减去 1
incl var(,1) ;将 var 所代表的 32 位整数加上 1.
  • imul 整数相乘

imul 指令有两种基本格式 : 第一种是 2 个参数的 ( 看下面语法开始两条 ); 第二种格式是 3 个参数的 ( 看下面语法最后两条 ).

2 个参数的这种格式, 先是将两个参数相乘, 然后把结果存到第二个参数中. 运算结果 ( 即第二个参数 ) 必须是一个寄存器.

3 个参数的这种格式, 先是将它的第 1 个参数和第 2 个参数相乘, 然后把结果存到第 3 个参数中, 当然, 第 3 个参数必须是一个寄存器. 此外, 第 1 个参数必须是一个常数.

  • 语法
imul <reg32, <reg32
imul <mem, <reg32
imul <con, <reg32, <reg32
imul <con, <mem, <reg32
  • 例子
imul (%ebx), %eax ;将 EAX 中的 32 位整数, 与 EBX 中的内容所指的内存单元, 相乘, 然后把结果存到 EAX 中.
imul $25, %edi, %esi ;ESI 被设置为 EDI * 25.
  • idiv 整数相除

idiv只有一个操作数,此操作数为除数,而被除数则为 EDX : EAX 中的内容(一个64位的整数), 除法结果 ( 商 ) 存在 EAX 中, 而所得的余数存在 EDX 中.

  • 语法
idiv <reg32
idiv <mem
  • 例子
idiv %ebx ;用 EDX : EAX 的值除以 EBX 的值. 商存放在 EAX 中, 余数存放在 EDX 中.
idivw (%ebx) ;将 EDX : EAX 的值除以存储在 EBX 所对应内存单元的 32 位值. 商存放在 EAX 中, 余数存放在 EDX 中.
  • and, or, xor 按位逻辑 与, 或, 异或 运算

这些指令分别对它们的参数进行相应的逻辑运算, 运算结果存到第一个参数中.

  • 语法
and <reg, <reg
and <mem, <reg
and <reg, <mem
and <con, <reg
and <con, <mem
or <reg, <reg
or <mem, <reg
or <reg, <mem
or <con, <reg
or <con, <mem
xor <reg, <reg
xor <mem, <reg
xor <reg, <mem
xor <con, <reg
xor <con, <mem
  • 例子
and $0x0F, %eax ;只留下 EAX 中最后 4 位数字 (二进制位)
xor %edx, %edx ;将 EDX 的值全部设置成 0
  • not 逻辑位运算 非

对参数进行逻辑非运算, 即翻转参数中所有位的值.

  • 语法
not <reg
not <mem
  • 例子
not %eax ;将 EAX 的所有值翻转.
  • neg 取负指令

取参数的二进制补码负数. 直接看例子也许会更好懂.

  • 语法
neg <reg
neg <mem
  • 例子
neg %eax ;EAX → -EAX
  • shl, shr 按位左移或者右移

这两个指令对第一个参数进行位运算, 移动的位数由第二个参数决定, 移动过后的空位拿 0 补上.被移的参数最多可以被移 31 位. 第二个参数可以是 8 位常数或者寄存器 CL. 在任意情况下, 大于 31 的移位都默认是与 32 取模.

  • 语法
shl <con8, <reg
shl <con8, <mem
shl %cl, <reg
shl %cl, <mem
shr <con8, <reg
shr <con8, <mem
shr %cl, <reg
shr %cl, <mem
  • 例子
shl $1, %eax ;将 EAX 的值乘以 2 (如果最高有效位是 0 的话)
shr %cl, %ebx ;将 EBX 的值除以 2n, 其中 n 为 CL 中的值, 运算最终结果存到 EBX 中.
3.3.4.3 流程控制指令

x86 处理器有一个指令指针寄存器 ( EIP ), 该寄存器为 32 位寄存器, 它用来在内存中指示我们输入汇编指令的位置. 就是说这个寄存器指向哪个内存单元, 那个单元存储的机器码就是程序执行的指令. 通常它是指向我们程序要执行的 下一条指令. 但是你不能直接操作 EIP 寄存器, 你需要流程控制指令来隐式地给它赋值.

我们使用符号 <label 来当作程序中的标签. 通过输入标签名称后跟冒号, 可以将标签插入 x86 汇编代码文本中的任何位置. 例如 :

       mov 8(%ebp), %esi
begin:
       xor %ecx, %ecx
       mov (%esi), %eax

该代码片段中的第二段被套上了 “ begin “ 这个标签. 在代码的其它地方, 我们可以用 “ begin “ 这个标签从而更方便地来引用这段指令在内存中的位置. 这个标签只是用来更方便地表示位置的, 它并不是用来代表某个 32 位值.

  • jmp 跳转指令

    将程序跳转到参数指定的内存地址, 然后执行该内存地址的指令.

  • 语法

jmp <label
  • 例子
jmp begin ;跳转到打了 " begin " 这个标签的地方

jmp

  • jcondition 有条件的跳转

这些指令是条件跳转指令, 它们基于一组条件代码的状态, 这些条件代码的状态存放在称为机器状态字 ( machine status word ) 的特殊寄存器中. 机器状态字的内容包括关于最后执行的算术运算的信息. 例如, 这个字的一个位表示最后的结果是否为 0. 另一个位表示最后结果是否为负数. 基于这些条件代码, 可以执行许多条件跳转. 例如, 如果最后一次算术运算结果为 0, 则 jz 指令就是跳转到指定参数标签. 否则, 程序就按照流程进入下一条指令.

许多条件分支的名称都是很直观的, 这些指令的运行, 都和一个特殊的比较指令有关, cmp( 见下文 ). 例如, 像 jlejne 这种指令, 它们首先对参数进行 cmp 操作.

  • 语法
je <label ;当相等的时候跳转
jne <label ;当不相等的时候跳转
jz <label ;当最后结果为 0 的时候跳转
jg <label ;当大于的时候跳转
jge <label ;当大于等于的时候跳转
jl <label ;当小于的时候跳转
jle <label ;当小于等于的时候跳转
  • 例子
cmp %ebx, %eax
jle done
;如果 EAX 的值小于等于 EBX 的值, 就跳转到 " done " 标签, 否则就继续执行下一条指令.
  • cmp 比较指令

比较两个参数的值, 适当地设置机器状态字中的条件代码. 此指令与sub指令类似,但是cmp不用将计算结果保存在操作数中.

  • 语法
cmp <reg, <reg
cmp <mem, <reg
cmp <reg, <mem
cmp <con, <reg
  • 例子
cmpb $10, (%ebx)
jeq loop
;如果 EBX 的值等于整数常量 10, 则跳转到标签 " loop " 的位置.

cmp

  • call, ret 子程序调用与返回

这两个指令实现子程序的调用和返回. call 指令首先将当前代码位置推到内存中硬件支持的栈内存上 ( 请看 push 指令 ), 然后无条件跳转到标签参数指定的代码位置. 与简单的 jmp 指令不同, call 指令保存了子程序完成时返回的位置. 就是 call 指令结束后, 返回到调用之前的地址.

ret 指令实现子程序的返回. 该指令首先从栈中取出代码 ( 类似于 pop 指令 ). 然后它无条件跳转到检索到的代码位置.

  • 语法
call <label
ret

3.3.5 调用约定 Calling Convention

为了方便不同的程序员去分享代码和运行库, 并简化一般子程序的使用, 程序员们通常会遵守一定的约定 ( Calling Convention ). 调用约定是关于如何从例程调用和返回的协议. 例如,给定一组调用约定规则,程序员不需要检查子例程的定义来确定如何将参数传递给该子例程. 此外,给定一组调用约定规则,可以使高级语言编译器遵循规则,从而允许手动编码的汇编语言例程和高级语言例程相互调用.

我们将讲解被广泛使用的 C 语言调用约定. 遵循此约定将允许您编写可从 C ( 和C ++ ) 代码安全地调用的汇编语言子例程, 并且还允许您从汇编语言代码调用 C 函数库.

C 调用约定很大程度上取决于使用硬件支持的栈内存. 它基于 push, pop, callret 指令. 子程序的参数在栈上传递. 寄存器保存在栈中, 子程序使用的局部变量放在栈中. 在大多数处理器上实现的高级过程语言都使用了类似的调用约定.

调用约定分为两组. 第一组规则是面向子例程的调用者 ( Caller ) 的, 第二组规则面向子例程的编写者, 即被调用者 ( Callee ). 应该强调的是, 错误地遵守这些规则会导致程序的致命错误, 因为栈将处于不一致的状态; 因此, 在你自己的子例程中实现调用约定的时候, 务必当心.

stack-convention

将调用约定可视化的一种好方法是, 在子例程执行期间画一个栈内存附近的图. 图 2 描绘了在执行具有三个参数和三个局部变量的子程序期间栈的内容. 栈中描绘的单元都是 32 位内存单元, 因此这些单元的内存地址相隔 4 个字节. 第一个参数位于距基指针 8 个字节的偏移处. 在栈参数的上方 ( 和基指针下方 ), call 指令在这放了返回地址, 从而导致从基指针到第一个参数有额外 4 个字节的偏移量. 当 ret 指令用于从子程序返回时, 它将跳转到栈中的返回地址.

3.3.5.1 调用者约定 Caller Rules

要进行子程序调用, 调用者应该 :

  1. 在调用子例程之前, 调用者应该保存指定调用者保存 ( Caller-saved )的某些寄存器的内容. 调用者保存的寄存器是 EAX, ECX, EDX. 由于被调用的子程序可以修改这些寄存器, 所以如果调用者在子例程返回后依赖这些寄存器的值, 调用者必须将这些寄存器的值入栈, 然后就可以在子例程返回后恢复它们.
  2. 要把参数传递给子例程, 你可以在调用之前把参数入栈. 参数的入栈顺序应该是反着的, 就是最后一个参数应该最先入栈. 随着栈内存地址增大, 第一个参数将存储在最低的地址, 在历史上, 这种参数的反转用于允许函数传递可变数量的参数.
  3. 要调用子例程, 请使用call指令. 该指令将返回地址存到栈上, 并跳转到子程序的代码. 这个会调用子程序, 这个子程序应该遵循下面的被调用者约定.

子程序返回后 ( 紧跟调用指令后 ), 调用者可以期望在寄存器 EAX 中找到子例程的返回值. 要恢复机器状态 ( machine state ), 调用者应该 :

  1. 从栈中删除参数, 这会把栈恢复到调用之前的状态.
  2. 把 EAX, ECX, EDX 之前入栈的内容给出栈, 调用者可以假设子例程没有修改其它寄存器.
  • 例子

下面的代码就是个活生生的例子, 它展示了遵循约定的函数调用. 调用者正在调用一个带有 3 个整数参数的函数 myFunc. 第一个参数是 EAX, 第二个参数是常数 216; 第三个参数位于 EBX 的值所代表的内存地址.

push (%ebx) ;最后一个参数最先入栈
push $216 ;把第二个参数入栈
push %eax ;第一个参数最后入栈
call myFunc ;调用这个函数 ( 假设以 C 语言的模式命名 )
add $12, %esp

注意, 在调用返回后, 调用者使用 add 指令来清理栈内存. 我们栈内存中有 12 个字节 ( 3 个参数, 每个参数 4 个字节 ), 然后栈内存地址增大. 因此, 为了摆脱掉这些参数, 我们可以直接往栈里面加个 12.

myFunc 生成的结果现在可以有用于寄存器 EAX. 调用者保存 ( Caller-saved ) 的寄存器 ( ECX, EDX ) 的值可能已经被修改. 如果调用者在调用之后使用它们,则需要在调用之前将它们保存在堆栈中并在调用之后恢复它们. 说白了就是把栈这个玩意当作临时存放点.

3.3.5.2 被调用者约定 Callee Rules

子例程的定义应该遵循子例程开头的以下规则 :

  • 1.将 EBP 的值入栈, 然后用下面的指示信息把 ESP 的值复制到 EBP 中 :
 push %ebp
 mov  %esp, %ebp

这个初始操作保留了基指针 EBP. 按照约定, 基指针作为栈上找到参数和变量的参考点. 当子程序正在执行的时候, 基指针保存了从子程序开始执行是的栈指针值的副本. 参数和局部变量将始终位于远离基指针值的已知常量偏移处. 我们在子例程的开头推送旧的基指针值,以便稍后在子例程返回时为调用者恢复适当的基指针值. 记住, 调用者不希望子例程修改基指针的值. 然后我们把栈指针移动到 EBP 中, 以获取访问参数和局部变量的参考点.

  • 2.接下来, 通过在栈中创建空间来分配局部变量. 回想一下, 栈会向下增长, 因此要在栈顶部创建空间, 栈指针应该递减. 栈指针递减的数量取决于所需局部变量的数量和大小. 例如, 如果需要 3 个局部整数 ( 每个 4 字节 ), 则需要将堆栈指针递减 12, 从而为这些局部变量腾出空间 ( 即sub $12, %esp ). 和参数一样, 局部变量将位于基指针的已知偏移处.
  • 3.接下来, 保存将由函数使用的 被调用者保存的 ( Callee-saved ) 寄存器的值. 要存储寄存器, 请把它们入栈. 被调用者保存 ( Callee-saved ) 的寄存器是 EBX, EDI 和 ESI ( ESP 和 EBP 也将由调用约定保留, 但在这个步骤中不需要入栈 ).

在完成这 3 步之后, 子例程的主体可以继续. 返回子例程的时候, 必须遵循以下步骤 :

  1. 将返回值保存在 EAX 中.
  2. 恢复已经被修改的任何被调用者保存 ( Callee-saved ) 的寄存器 ( EDI 和 ESI ) 的旧值. 通过出栈来恢复它们. 当然应该按照相反的顺序把它们出栈.
  3. 释放局部变量. 显而易见的法子是把相应的值添加到栈指针 ( 因为空间是通过栈指针减去所需的数量来分配的 ). 事实上呢, 解除变量释放的错误的方法是将基指针中的值移动到栈指针 : mov %ebp, %esp. 这个法子有效, 是因为基指针始终包含栈指针在分配局部变量之前包含的值.
  4. 在返回之前, 立即通过把 EBP 出栈来恢复调用者的基指针值. 回想一下, 我们在进入子程序的时候做的第一件事是推动基指针保存它的旧值.
  5. 最后, 通过执行 ret 指令返回. 这个指令将从栈中找到并删除相应的返回地址 ( call 指令保存的那个 ).

请注意, 被调用者的约定完全被分成了两半, 简直是彼此的镜像. 约定的前半部分适用于函数开头, 并且通常被称为定义函数的序言 ( prologue ) .这个约定的后半部分适用于函数结尾, 因此通常被称为定义函数的结尾 ( epilogue ).

  • 例子

这是一个遵循被调用者约定的例子 :

;启动代码部分
.text
;将 myFunc 定义为全局 ( 导出 ) 函数
.globl myFunc
.type myFunc, @function
myFunc :
;子程序序言
push %ebp ;保存基指针旧值
mov %esp, %ebp ;设置基指针新值
sub $4, %esp ;为一个 4 字节的变量腾出位置
push %edi
push %esi ;这个函数会修改 EDI 和 ESI, 所以先给它们入栈
;不需要保存 EBX, EBP 和 ESP
;子程序主体
mov 8(%ebp), %eax ;把参数 1 的值移到 EAX 中
mov 12(%ebp), %esi ;把参数 2 的值移到 ESI 中
mov 16(%ebp), %edi ;把参数 3 的值移到 EDI 中
mov %edi, -4(%ebp) ;把 EDI 移给局部变量
add %esi, -4(%ebp) ;把 ESI 添加给局部变量
add -4(%ebp), %eax ;将局部变量的内容添加到 EAX ( 最终结果 ) 中
;子程序结尾
pop %esi ;恢复寄存器的值
pop %edi
mov %ebp, %esp ;释放局部变量
pop %ebp ;恢复调用者的基指针值
ret

子程序序言执行标准操作, 即在 EBP ( 基指针 ) 中保存栈指针的副本, 通过递减栈指针来分配局部变量, 并在栈上保存寄存器的值.

在子例程的主体中, 我们可以看到基指针的使用. 在子程序执行期间, 参数和局部变量都位于与基指针的常量偏移处. 特别地, 我们注意到, 由于参数在调用子程序之前被放在栈中, 因此它们总是位于栈基指针 ( 即更高的地址 ) 之下. 子程序的第一个参数总是可以在内存地址 ( EBP+8 ) 找到, 第二个参数在 ( EBP+12 ), 第三个参数在 ( EBP+16). 类似地, 由于在设置基指针后分配局部变量, 因此它们总是位于栈上基指针 ( 即较低地址 ) 之上. 特别是, 第一个局部变量总是位于 ( EBP-4 ), 第二个位于 ( EBP-8 ), 以此类推. 这种基指针的常规使用, 让我们可以快速识别函数内部局部变量和参数的使用.

函数结尾基本上是函数序言的镜像. 从栈中恢复调用者的寄存器值, 通过重置栈指针来释放局部变量, 恢复调用者的基指针值, 并用 ret 指令返回调用者中的相应代码位置, 从哪来回哪去.

维基百科 X86 调用约定

3.4 x64 汇编基础

3.4.1 导语

x86-64 (也被称为 x64 或者 AMD64) 是 64 位版本的 x86/IA32 指令集. 以下是我们关于 CS107 相关功能的概述.

3.4.2 寄存器 Registers

下图列出了常用的寄存器 ( 16个通用寄存器加上 2 个特殊用途寄存器 ). 每个寄存器都是 64 bit 宽, 它们的低 32, 16, 8 位都可以看成相应的 32, 16, 8 位寄存器, 并且都有其特殊名称. 一些寄存器被设计用来完成某些特殊目的, 比如 %rsp 被用来作为栈指针, %rax 作为一个函数的返回值. 其他寄存器则都是通用的, 但是一般在使用的时候, 还是要取决于调用者 ( Caller-owned )或者被调用者 ( Callee-owned ). 如果函数 binky 调用了 winky, 我们称 binky 为调用者, winky 为被调用者. 例如, 用于前 6 个参数和返回值的寄存器都是被调用者所有的 ( Callee-owned ). 被调用者可以任意使用这些寄存器, 不用任何预防措施就可以随意覆盖里面的内容. 如果 %rax 存着调用者想要保留的值, 则 Caller 必须在调用之前将这个 %rax 的值复制到一个 “ 安全 “ 的位置. 被调用者拥有的 ( Callee-owned ) 寄存器非常适合一些临时性的使用. 相反, 如果被调用者打算使用调用者所拥有的寄存器, 那么被调用者必须首先把这个寄存器的值存起来, 然后在退出调用之前把它恢复. 调用者拥有的 ( Caller-owned ) 寄存器用于保存调用者的本地状态 ( local state ), 所以这个寄存器需要在进一步的函数调用中被保留下来.

3.4.3 寻址模式 Addressing modes

正由于它的 CISC 特性, X86-64 支持各种寻址模式. 寻址模式是计算要读或写的内存地址的表达式. 这些表达式用作mov指令和访问内存的其它指令的来源和去路. 下面的代码演示了如何在每个可用的寻址模式中将 立即数 1 写入各种内存位置 :

movl $1, 0x604892         ;直接写入, 内存地址是一个常数
movl $1, (%rax)           ;间接写入, 内存地址存在寄存器 %rax 中
movl $1, -24(%rbp)       ;使用偏移量的间接写入
                         ;公式 : (address = base %rbp + displacement -24)
movl $1, 8(%rsp, %rdi, 4) ;间接写入, 用到了偏移量和按比例放大的索引 ( scaled-index )
           ;公式 : (address = base %rsp + displ 8 + index %rdi * scale 4)
movl $1, (%rax, %rcx, 8) ;特殊情况, 用到了按比例放大的索引 ( scaled-index ), 假设偏移量 ( displacement ) 为 0
movl $1, 0x8(, %rdx, 4)  ;特殊情况, 用到了按比例放大的索引 ( scaled-index ), 假设基数 ( base ) 为 0
movl $1, 0x4(%rax, %rcx) ;特殊情况, 用到了按比例放大的索引 ( scaled-index ), 假设比例 ( scale ) 为0

3.4.4 通用指令 Common instructions

先说下指令后缀, 之前讲过这里就重温一遍 : 许多指令都有个后缀 ( b, w, l, q ) , 后缀指明了这个指令代码所操纵参数数据的位宽 ( 分别为 1, 2, 4 或 8 个字节 ). 当然, 如果可以从参数确定位宽的时候, 后缀可以被省略. 例如呢, 如果目标寄存器是 %eax, 则它必须是 4 字节宽, 如果是 %ax 寄存器, 则必须是 2 个字节, 而 %al 将是 1 个字节. 还有些指令, 比如 movsmovz 有两个后缀 : 第一个是来源参数, 第二个是去路. 这话乍一看让人摸不着头脑, 且听我分析. 例如, movzbl 这个指令把 1 个字节的来源参数值移动到 4 个字节的去路.

当目标是子寄存器 ( sub-registers ) 时, 只有子寄存器的特定字节被写入, 但有一个例外 : 32 位指令将目标寄存器的高 32 位设置为 0.

movlea 指令

到目前为止, 我们遇到的最频繁的指令就是 mov, 而它有很多变种. 关于 mov 指令就不多说了, 和之前 32 位 x86 的没什么区别. lea 指令其实也没什么好说的, 上一节都有, 这里就不废话了.

这里写几个比较有意思的例子 :
mov 8(%rsp), %eax    ;%eax = 从地址 %rsp + 8 读取的值
lea 0x20(%rsp), %rdi ;%rdi = %rsp + 0x20
lea (%rdi,%rdx,1), %rax  ;%rax = %rdi + %rdx

在把较小位宽的数据移动复制到较大位宽的情况下, movsmovz 这两个变种指令用于指定怎么样去填充字节, 因为你是一个小东西被移到了一个大空间, 肯定还有地方是空的, 所以空的地方要填起来, 拿 0 或者 符号扩展 ( sign-extend ) 来填充.

movsbl %al, %edx     ;把 1 个字节的 %al, 符号扩展 复制到 4 字节的 %edx
movzbl %al, %edx     ;把 1 个字节的 %al, 零扩展 ( zero-extend ) 复制到 4 字节的 %edx

有个特殊情况要注意, 默认情况下, 将 32 位值写入寄存器的 mov 指令, 也会将寄存器的高 32 位归零, 即隐式零扩展到位宽 q. 这个解释了诸如 mov %ebx, %ebx 这种指令, 这些指令看起来很奇怪, 但实际上这是用于从 32 位扩展到 64 位. 因为这个是默认的, 所以我们不用显式的 movzlq 指令. 当然, 有一个 movslq 指令也是从 32 位符号扩展到 64 位.

cltq 指令是一个在 %rax 上运行的专用移动指令. 这个没有参数的指令在 %rax 上进行符号扩展, 源位宽为 L, 目标位宽为 q.

cltq   ;在 %rax 上运行,将 4 字节 src 符号扩展为 8 字节 dst,用于 movslq %eax,%rax
算术和位运算

二进制的运算一般是两个参数, 其中第二个参数既是我们指令运算的来源, 也是去路的来源, 就是说我们把运算结果存在第二个参数里. 我们的第一个参数可以是立即数常数, 寄存器或者内存单元. 第二个参数必须是寄存器或者内存. 这两个参数中, 最多只有一个参数是内存单元, 当然也有的指令只有一个参数, 这个参数既是我们运算数据的来源, 也是我们运算数据的去路, 它可以是寄存器或者内存. 这个我们上一节讲了, 这里回顾一下. 许多算术指令用于有符号和无符号类型,也就是带符号加法和无符号加法都使用相同的指令. 当需要的时候, 参数设置的条件代码可以用来检测不同类型的溢出.

add src, dst ;dst = dst + src
sub src, dst ;dst = dst - src
imul src, dst ;dst = dst * src
neg dst ;dst = -dst ( 算术取反 )
and src, dst ;dst = dst & src
or src, dst ;dst = dst | src
xor src, dst ;dst = dst ^ src
not dst ;dst = ~dst ( 按位取反 )
shl count, dst ;dst <<= count ( 按 count 的值来左移 ), 跟这个相同的是`sal`指令
sar count, dst ;dst = count ( 按 count 的值来算术右移 )
shr count, dst ;dst = count ( 按 count 的值来逻辑右移 )
;某些指令有特殊情况变体, 这些变体有不同的参数
imul src ;一个参数的 imul 指令假定 %rax 中其他参数计算 128 位的结果, 在 %rdx 中存储高 64 位, 在 %rax 中存储低 64 位.
shl dst ;dst <<= 1 ( 后面没有 count 参数的时候默认是移动 1 位, `sar`, `shr`, `sal` 指令也是一样 )

这些指令上一节都讲过, 这里稍微提一下.

流程控制指令

有一个特殊的 %eflags 寄存器, 它存着一组被称为条件代码的布尔标志. 大多数的算术运算会更新这些条件代码. 条件跳转指令读取这些条件代码之后, 再确定是否执行相应的分支指令. 条件代码包括 ZF( 零标志 ), SF( 符号标志 ), OF( 溢出标志, 有符号 ) 和 CF( 进位标志, 无符号 ). 例如, 如果结果为 0 , 则设置 ZF, 如果操作溢出 ( 进入符号位 ), 则设置 OF.

这些指令一般是先执行 cmptest 操作来设置标志, 然后再跟跳转指令变量, 该变量读取标志来确定是采用分支代码还是继续下一条代码. cmptest 的参数是立即数, 寄存器或者内存单元 ( 最多只有一个内存参数 ). 条件跳转有 32 中变体, 其中几种效果是一样的. 下面是一些分支指令.

cmpl op2, op1 ;运算结果 = op1 - op2, 丢弃结果然后设置条件代码
test op2, op1 ;运算结果 = op1 & op2, 丢弃结果然后设置条件代码
jmp target ;无条件跳跃
je target ;等于时跳跃, 和它相同的还有 jz, 即jump zero ( ZF = 1 )
jne target ;不相等时跳跃, 和它相同的还有 jnz, 即 jump non zero ( ZF = 0 )
jl target ;小于时跳跃, 和它相同的还有 jnge, 即 jump not greater or equal ( SF != OF )  
jle target ;小于等于时跳跃, 和它相同的还有 jng, 即 jump not greater ( ZF = 1 or SF != OF )
jg target ;大于时跳跃, 和它相同的还有 jnle, 即 jump not less or equal ( ZF = 0 and SF = OF )
jge target ;大于等于时跳跃, 和它相同的还有 jnl, 即 jump not less ( SF = OF )
ja  target ;跳到上面, 和它相同的还有 jnbe, 即 jump not below or equal ( CF = 0 and ZF = 0 )
jb  target ;跳到下面, 和它相同的还有 jnae, 即 jump not above or equal ( CF = 1 )
js  target ;SF = 1 时跳跃
jns target ;SF = 0 时跳跃

其实你也会发现这里大部分上一节都讲过, 这里我们可以再来一遍巩固一下.

setxmovx

还有两个指令家族可以 读取/响应 当前的条件代码. setx 指令根据条件 x 的状态将目标寄存器设置为 0 或 1. cmovx 指令根据条件 x 是否成立来有条件地执行 mov. x 是任何条件变量的占位符, 就是说 x 可以用这些来代替 : e, ne, s, ns. 它们的意思上面也都说过了.

sete dst ;根据 零/相等( zero/equal ) 条件来把 dst 设置成 0 或 1
setge dst ;根据 大于/相等( greater/equal ) 条件来把 dst 设置成 0 或 1
cmovns src, dst ;如果 ns 条件成立, 则继续执行 mov
cmovle src, dst ;如果 le 条件成立, 则继续执行 mov

对于 setx 指令, 其目标必须是单字节寄存器 ( 例如 %al 用于 %rax 的低字节 ). 对于 cmovx 指令, 其来源和去路都必须是寄存器.

函数调用与栈

%rsp 寄存器用作 “ 栈指针 “; pushpop 用于添加或者删除栈内存中的值. push 指令只有一个参数, 这个参数是立即数常数, 寄存器或内存单元. push 指令先把 %rsp 的值递减, 然后将参数复制到栈内存上的 tompost. pop 指令也只有一个参数, 即目标寄存器. pop 先把栈内存最顶层的值复制到目标寄存器, 然后把 %rsp 递增. 直接调整 %rsp, 以通过单个参数添加或删除整个数组或变量集合也是可以的. 但注意, 栈内存是朝下增长 ( 即朝向较低地址 ).

push %rbx ;把 %rbx 入栈
pushq $0x3 ;把立即数 3 入栈
sub $0x10, %rsp ;调整栈指针以空出 16 字节
pop %rax ;把栈中最顶层的值出栈到寄存器 %rax 中
add $0x10, %rsp ;调整栈指针以删除最顶层的 16 个字节

函数之间是通过互相调用返回来互相控制的. callq 指令有一个参数, 即被调用的函数的地址. 它将返回来的地址入栈, 这个返回来的地址即 %rip 当前的值, 也即是调用函数后的下一条指令. 然后这个指令让程序跳转到被调用的函数的地址. retq 指令把刚才入栈的地址给出栈, 让它回到 %rip 中, 从而让程序在保存的返回地址处重新开始, 就是说你中途跳到别的地方去, 你回来的时候要从你跳的那个地方重新开始.

当然, 你如果要设置这种函数间的互相调用, 调用者需要将前六个参数放入寄存器 %rdi, %rsi, %rdx, %rcx, %r8 和 %r9 ( 任何其它参数都入栈 ), 然后再执行调用指令.

mov $0x3, %rdi ;第一个参数在 %rdi 中
mov $0x7, %rsi ;第二个参数在 %rsi 中
callq binky ;把程序交给 binky 控制

当被调用者那个函数完事的时候, 这个函数将返回值 ( 如果有的话 ) 写入 %rax, 然后清理栈内存, 并使用 retq 指令把程序控制权交还给调用者.

mov $0x0, %eax ;将返回值写入 %rax
add $0x10, %rsp ;清理栈内存
retq ;交还控制权, 跳回去

这些分支跳转指令的目标通常是在编译时确定的绝对地址. 但是, 有些情况下直到运行程序的时候, 我们才知道目标的绝对内存地址. 例如编译为跳转表的 switch 语句或调用函数指针时. 对于这些, 我们先计算目标地址, 然后把地址存到寄存器中, 然后用 分支/调用( branch/call ) 变量 je *%raxcallq *%rax 从指定寄存器中读取目标地址.

当然还有更简单的方法, 就是上一节讲的打标签.

3.4.5 汇编和 gdb

调试器 ( debugger ) 有许多功能, 这可以让你可以在程序中追踪和调试代码. 你可以通过在其名称上加个 $ 来打印寄存器中的值, 或者使用命令 info reg 转储所有寄存器的值 :

(gdb) p $rsp
(gdb) info reg

disassemble 命令按照名称打印函数的反汇编. x 命令支持 i 格式, 这个格式把内存地址的内容解释为编码指令 ( 解码 ).

(gdb) disassemble main //反汇编, 然后打印所有 main 函数的指令
(gdb) x/8i main //反汇编, 然后打印开始的 8 条指令

你可以通过在函数中的直接地址或偏移量为特定汇编指令设置断点.

(gdb) b *0x08048375
(gdb) b *main+7 //在 main+7个字节这里设置断点

你可以用 stepinexti 命令来让程序通过指令 ( 而不是源代码 ) 往前执行.

(gdb) stepi
(gdb) nexti

3.5 ARM汇编基础

3.5.1 引言

本章所讲述的是在 GNU 汇编程序下的 ARM 汇编快速指南,而所有的代码示例都会采用下面的结构:

[< 标签 label :]  {<指令 instruction or directive } @ 注释 comment

在 GNU 程序中不需要缩进指令。程序的标签是由冒号识别而与所处的位置无关。 就通过一个简单的程序来介绍:

.section .text, "x"
.global   add @给符号添加外部链接
add:
       ADD    r0, r0, r1    @添加输入参数
​      MOV    pc, lr         @从子程序返回
                            @程序结束

它定义的是一个返回总和函数 “ add ”,允许两个输入参数。通过了解这个程序实例,想必接下来这类程序的理解我们也能够很好的的掌握。

3.5.2 ARM 的 GNU 汇编程序指令表

在 GNU 汇编程序下的 ARM 指令集涵括如下:

GUN 汇编程序指令描述
.ascii "<string>"将字符串作为数据插入到程序中
.asciz "<string>"与 .ascii 类似,但跟随字符串的零字节
.balign <power_of_2> {,<fill_value>{,<max_padding>} }将地址与 <power_of_2> 字节对齐。 汇编程序通过添加值 <fill_value> 的字节或合适的默认值来对齐. 如果需要超过 <max_padding> 这个数字来填充字节,则不会发生对齐( 类似于armasm 中的 ALIGN )
.byte <byte1> {,<byte2> } …将一个字节值列表作为数据插入到程序中
.code <number_of_bits>以位为单位设置指令宽度。 使用 16 表示 Thumb,32 表示 ARM 程序( 类似于 armasm 中的 CODE16 和 CODE32 )
.else与.if和 .endif 一起使用( 类似于 armasm 中的 ELSE )
.end标记程序文件的结尾( 通常省略 )
.endif结束条件编译代码块 - 参见.if,.ifdef,.ifndef( 类似于 armasm 中的 ENDIF )
.endm结束宏定义 - 请参阅 .macro( 类似于 armasm 中的 MEND )
.endr结束重复循环 - 参见 .rept 和 .irp(类似于 armasm 中的 WEND )
.equ <symbol name>, <vallue>该指令设置符号的值( 类似于 armasm 中的 EQU )
.err这个会导致程序停止并出现错误
.exitm中途退出一个宏 - 参见 .macro( 类似于 armasm 中的 MEXIT )
.global <symbol>该指令给出符号外部链接( 类似于 armasm 中的 MEXIT )。
.hword <short1> {,<short2> }...将16位值列表作为数据插入到程序中( 类似于 armasm 中的 DCW )
.if <logical_expression>把一段代码变成前提条件。 使用 .endif 结束代码块( 类似于 armasm中的 IF )。 另见 .else
.ifdef <symbol>如果定义了 <symbol>,则包含一段代码。 结束代码块用 .endif, 这就是个条件判断嘛, 很简单的.
.ifndef <symbol>如果未定义 <symbol>,则包含一段代码。 结束代码块用 .endif, 同上.
.include "<filename>"包括指定的源文件, 类似于 armasm 中的 INCLUDE 或 C 中的#include
.irp <param> {,<val 1>} {,<val_2>} ...为值列表中的每个值重复一次代码块。 使用 .endr 指令标记块的结尾。 在里面重复代码块,使用 \<param> 替换关联的代码块值列表中的值。
.macro <name> {<arg_1>} {,< arg_2>} ... {,<arg_N>}使用 N 个参数定义名为<name>的汇编程序宏。宏定义必须以 .endm 结尾。 要在较早的时候从宏中逃脱,请使用 .exitm。 这些指令是类似于 armasm 中的 MACRO,MEND 和MEXIT。 你必须在虚拟宏参数前面加 \.
.rept <number_of_times>重复给定次数的代码块。 以.endr结束。
<register_name> .req <register_name>该指令命名一个寄存器。 它与 armasm 中的 RN 指令类似,不同之处在于您必须在右侧提供名称而不是数字(例如,acc .req r0
.section <section_name> {,"<flags> "}启动新的代码或数据部分。 GNU 中有这些部分:.text代码部分;.data初始化数据部分和.bss未初始化数据部分。 这些部分有默认值flags和链接器理解默认名称(与armasm指令AREA类似的指令)。 以下是 ELF 格式文件允许的 .section标志:a 表示 allowable sectionw 表示 writable sectionx 表示 executable section
.set <variable_name>, <variable_value>该指令设置变量的值。 它类似于 SETA。
.space <number_of_bytes> {,<fill_byte> }保留给定的字节数。 如果指定了字节,则填充零或 <fill_byte>(类似于 armasm 中的 SPACE)
.word <word1> {,<word2>}...将 32 位字值列表作为数据插入到程序集中(类似于 armasm 中的 DCD)。

3.5.3 寄存器名称

通用寄存器:

%r0 - %r15

fp 寄存器:

%f0 - %f7

临时寄存器:

%r0 - %r3, %r12

保存寄存器:

%r4 - %r10

堆栈 ptr 寄存器:

%sp

帧 ptr 寄存器:

%fp

链接寄存器:

%lr

程序计数器:

%ip

状态寄存器:

$psw

状态标志寄存器:

xPSR

xPSR_all

xPSR_f

xPSR_x

xPSR_ctl

xPSR_fs

xPSR_fx

xPSR_fc

xPSR_cs

xPSR_cf

xPSR_cx

3.5.4 汇编程序特殊字符/语法

内联评论字符: ‘@’

行评论字符: ‘#’

语句分隔符: ‘;’

立即操作数前缀: ‘#’ 或 ‘$’

3.5.5 arm程序调用标准

参数寄存器 :%a0 - %a4(别名为%r0 - %r4)

返回值regs :%v1 - %v6(别名为%r4 - %r9)

3.5.6 寻址模式

addr 绝对寻址模式

%rn 寄存器直接寻址

[%rn] 寄存器间接寻址或索引

[%rn,#n] 基于寄存器的偏移量

上述 “rn” 指任意寄存器,但不包括控制寄存器。

3.5.7 机器相关指令

指令描述
.arm使用arm模式进行装配
.thumb使用thumb模式进行装配
.code16使用thumb模式进行装配
.code32使用arm模式进行组装
.force_thumb Forcethumb模式(即使不支持)
.thumb_func将输入点标记为thumb编码(强制bx条目)
.ltorg启动一个新的文字池

3.6 MIPS汇编基础

数据类型和常量

  • 数据类型:
    • 指令全是32位
    • 字节(8位),半字(2字节),字(4字节)
    • 一个字符需要1个字节的存储空间
    • 整数需要1个字(4个字节)的存储空间
  • 常量:
    • 按原样输入的数字。例如 4
    • 用单引号括起来的字符。例如 ‘b’
    • 用双引号括起来的字符串。例如 “A string”

寄存器

  • 32个通用寄存器
  • 寄存器前面有 $

两种格式用于寻址:

  • 使用寄存器号码,例如 $ 0$ 31
  • 使用别名,例如 $ t1$ sp
  • 特殊寄存器 Lo 和 Hi 用于存储乘法和除法的结果
    • 不能直接寻址; 使用特殊指令 mfhi( “ 从 Hi 移动 ” )和 mflo( “ 从 Lo 移动 ” )访问的内容
  • 栈从高到低增长
寄存器别名用途
$0$zero常量0(constant value 0)
$1$at保留给汇编器(Reserved for assembler)
$2-$3$v0-$v1函数调用返回值(values for results and expression evaluation)
$4-$7$a0-$a3函数调用参数(arguments)
$8-$15$t0-$t7暂时的(或随便用的)
$16-$23$s0-$s7保存的(或如果用,需要SAVE/RESTORE的)(saved)
$24-$25$t8-$t9暂时的(或随便用的)
$26~$27$k0~$k1保留供中断/陷阱处理程序使用
$28$gp全局指针(Global Pointer)
$29$sp堆栈指针(Stack Pointer)
$30$fp帧指针(Frame Pointer)
$31$ra返回地址(return address)

再来说一说这些寄存器 :

  • zero 它一般作为源寄存器,读它永远返回 0,也可以将它作为目的寄存器写数据,但效果等于白写。为什么单独拉一个寄存器出来返回一个数字呢?答案是为了效率,MIPS 的设计者只允许在寄存器内执行算术操作,而不允许直接操作立即数。所以对最常用的数字 0 单独留了一个寄存器,以提高效率
  • at 该寄存器为给编译器保留,用于处理在加载 16 位以上的大常数时使用,编译器或汇编程序需要把大常数拆开,然后重新组合到寄存器里。系统程序员也可以显式的使用这个寄存器,有一个汇编 directive 可被用来禁止汇编器在 directive 之后再使用 at 寄存器。
  • v0, v1.这两个很简单,用做函数的返回值,大部分时候,使用 v0 就够了。如果返回值的大小超过 8 字节,那就需要分配使用堆栈,调用者在堆栈里分配一个匿名的结构,设置一个指向该参数的指针,返回时 v0 指向这个对应的结构,这些都是由编译器自动完成。
  • a0-a3. 用来传递函数入参给子函数。看一下这个例子:ret = strncmp("bear","bearer",4)参数少于 16 字节,可以放入寄存器中,在 strncmp 的函数里,a0 存放的是 “bear” 这个字符串所在的只读区地址,a1 是 “bearer” 的地址,a2 是 4.
  • t0-t9 临时寄存器 s0-s8 保留寄存器这两种寄存器需要放在一起说,它们是 mips 汇编里面代码里见到的最多的两种寄存器,它们的作用都是存取数据,做计算、移位、比较、加载、存储等等,区别在于,t0-t9 在子程序中可以使用其中的值,并不必存储它们,它们很适合用来存放计算表达式时使用的“临时”变量。如果这些变量的使用要要跳转到子函数之前完成,因为子函数里很可能会使用相同的寄存器,而且不会有任何保护。如果子程序里不会调用其它函数那么建议尽量多的使用t0-t9,这样可以避免函数入口处的保存和结束时的恢复。相反的,s0-s8 在子程序的执行过程中,需要将它们存储在堆栈里,并在子程序结束前恢复。从而在调用函数看来这些寄存器的值没有变化。
  • k0, k1. 这两个寄存器是专门预留给异常处理流程中使用。异常处理流程中有什么特别的地方吗?当然。当 MIPS CPU 在任务里运行的时候,一旦有外部中断或者异常发生,CPU 就会立刻跳转到一个固定地址的异常 handler 函数执行,并同时将异常结束后返回到任务的指令地址记录在 EPC 寄存器(Exception Program Counter)里。习惯性的,异常 handler 函数开头总是会保持现场即 MIPS 寄存器到中断栈空间里,而在异常返回前,再把这些寄存器的值恢复回去。那就存在一个问题,这个 EPC 里的值存放在哪里?异常 handler 函数的最后肯定是一句 jr x,X 是一个 MIPS 寄存器,如果存放在前面提到的 t0,s0 等等,那么 PC 跳回任务执行现场时,这个寄存器里的值就不再是异常发生之前的值。所以必须要有时就可以一句 jr k0指令返回了。k1 是另外一个专为异常而生的寄存器,它可以用来记录中断嵌套的深度。CPU 在执行任务空间的代码时,k1 就可以置为 0,进入到中断空间,每进入一次就加 1,退出一次相应减 1,这样就可以记录中断嵌套的深度。这个深度在调试问题的时候经常会用到,同时应用程序在做一次事情的时候可能会需要知道当前是在任务还是中断上下文,这时,也可以通过 k1 寄存器是否为 0 来判断。
  • sp 指向当前正在操作的堆栈顶部,它指向堆栈中的下一个可写入的单元,如果从栈顶获取一个字节是 sp-1 地址的内容。在有 RTOS 的系统里,每个 task 都有自己的一个堆栈空间和实时 sp 副本,中断也有自己的堆栈空间和 sp 副本,它们会在上下文切换的过程中进行保存和恢复。
  • gp 这是一个辅助型的寄存器,其含义较为模糊,MIPS 官方为该寄存器提供了两个用法建议,一种是指向 Linux 应用中位置无关代码之外的数据引用的全局偏移量表;在运行 RTOS 的小型嵌入式系统中,它可以指向一块访问较为频繁的全局数据区域,由于MIPS 汇编指令长度都是 32bit,指令内部的 offset 为 16bit,且为有符号数,所以能用一条指令以 gp 为基地址访问正负 15bit 的地址空间,提高效率。那么编译器怎么知道gp初始化的值呢?只要在 link 文件中添加 _gp 符号,连接器就会认为这是 gp 的值。我们在上电时,将 _gp 的值赋给 gp 寄存器就行了。话说回来,这都是 MIPS 设计者的建议,不是强制,楼主还见过一种 gp 寄存器的用法,来在中断和任务切换时做 sp 的存储过渡,也是可以的。
  • fp 这个寄存器不同的编译器对其解释不同,GNU MIPS C 编译器使用其作为帧指针,指向堆栈里的过程帧(一个子函数)的第一个字,子函数可以用其做一个偏移访问栈帧里的局部变量,sp 也可以较为灵活的移动,因为在函数退出之前使用 fp 来恢复;还要一种而 SGI 的 C 编译器会将这个寄存器直接作为 s8,扩展了一个保留寄存器给编译器使用。
  • ra 在函数调用过程中,保持子函数返回后的指令地址。汇编语句里函数调用的形式为:jal function_X这条指令 jal(jump-and-link,跳转并链接) 指令会将当期执行运行指令的地址 +4 存储到 ra 寄存器里,然后跳转到 function_X 的地址处。相应的,子函数返回时,最常见的一条指令就是jr rara 是一个对于调试很有用的寄存器,系统的运行的任何时刻都可以查看它的值以获取 CPU 的运行轨迹。

最后,如果纯写汇编语句的话,这些寄存器当中除了 zero 之外,其它的基本上都可以做普通寄存器存取数据使用(这也是它们为什么会定义为“通用寄存器”,而不像其它的协处理器、或者外设的都是专用寄存器,其在出厂时所有的功能都是定死的),那为什么有这么多规则呢 ?MIPS 开发者们为了让自己的处理器可以运行像 C、Java 这样的高级语言,以及让汇编语言和高级语言可以安全的混合编程而设计的一套 ABI(应用编程接口),不同的编译器的设计者们就会有据可依,系统程序员们在阅读、修改汇编程序的时候也能根据这些约定而更为顺畅地理解汇编代码的含义。

程序结构

  • 本质上只是带有数据声明的纯文本文件,程序代码 ( 文件名应以后缀 .s 结尾,或者.asm )
  • 数据声明部分后跟程序代码部分
数据声明
  • 数据以 .data 为标识
  • 声明变量后,即在内存中分配空间
代码
  • 放在用汇编指令 .text 标识的文本部分中
  • 包含程序代码( 指令 )
  • 给定标签 main 代码执行的起点 ( 和 C 语言一样 )
  • 程序结束标志(见下面的系统调用)
注释
  • # 表示单行注释

# 后面的任何内容都会被视为注释

  • MIPS 汇编语言程序的模板:
#给出程序名称和功能描述的注释
#Template.s
#MIPS汇编语言程序的Bare-bones概述
            .data #变量声明遵循这一行
                        #...
            .text#指令跟随这一行
 main:#表示代码的开始(执行的第一条指令)
                        #...
#程序结束,之后留空,让SPIM满意.
变量声明

声明格式:

name:storage_type value(s)

使用给定名称和指定值为指定类型的变量创建空间

value (s) 通常给出初始值; 对于.space,给出要分配的空格数

注意:标签后面跟冒号(😃

  • 例如
var1:.word 3 #创建一个初始值为 3 的整数变量
array1:.byte'a','b' #创建一个元素初始化的 2 元素字符数组到 a 和 b
array2:.space 40  #分配 40 个连续字节, 未初始化的空间可以用作 40 个元素的字符数组, 或者是
                                   #10 个元素的整数数组.
读取/写入 ( Load/Store )指令
  • 对 RAM 的访问, 仅允许使用加载和存储指令 ( 即 load 或者 store)
  • 所有其他指令都使用寄存器参数

load

lw register_destination,RAM_source
#将源内存地址的字 ( 4 个字节 ) 复制到目标寄存器,(lw中的'w'意为'word',即该数据大小为4个字节)
lb register_destination,RAM_source
#将源内存地址的字节复制到目标寄存器的低位字节, 并将符号映射到高位字节 ( 同上, lb 意为 load byte )

store

sw register_source,RAM_destination
#将源寄存器的字存储到目标内存RAM中
sb register_source,RAM_destination
#将源寄存器中的低位字节存储到目标内存RAM中

立即加载:

li register_destination,value
#把立即值加载到目标寄存器中,顾名思义, 这里的 li 意为 load immediate, 即立即加载.
  • 例子
       .data
var1:  .word  23            # 给变量 var1 在内存中开辟空间, 变量初始值为 23
       .text
__start:
       lw     $t0, var1            # 将内存单元中的内容加载到寄存器中 $t0:  $t0 = var1
       li     $t1, 5               #  $t1 = 5   ("立即加载")
       sw     $t1, var1            # 把寄存器$t1的内容存到内存中 : var1 = $t1
       done
间接和立即寻址
  • 仅用于读取和写入指令

    *直接给地址:*

       la $t0,var1
  • 将 var1 的内存地址(可能是程序中定义的标签)复制到寄存器 $t0

    *间接寻址, 地址是寄存器的内容, 类似指针:*

       lw $t2,($t0)
  • $t0 中包含的 RAM 地址加载到 $t2
       sw $t2,($t0)
  • $t2 寄存器中的字存储到 $t0 中包含的地址的 RAM 中

    *基于偏移量的寻址:*

       lw $t2, 4($t0)
  • 将内存地址 ( $t0 + 4 ) 的字加载到寄存器 $t2
  • “ 4 ” 给出了寄存器 $t0 中地址的偏移量
       sw $t2,-12($t0)
  • 将寄存器 $t2 中的字放到内存地址( $t0 - 12

  • 负偏移也是可以的, 反向漂移方不方 ?

    注意:基于偏移量 的寻址特别适用于:

  • 数组; 访问元素作为与基址的偏移量

  • 栈; 易于访问偏离栈指针或帧指针的元素

  • 例子

 .data
 array1:             .space 12            #  定义一个 12字节 长度的数组 array1, 容纳 3个整型
              .text
 __start:     la     $t0, array1          #  让 $t0 = 数组首地址
              li     $t1, 5               #  $t1 = 5   ("load immediate")
              sw $t1, ($t0)               #  数组第一个元素设置为 5; 用的间接寻址; array[0] = $1 = 5
              li $t1, 13                  #   $t1 = 13
              sw $t1, 4($t0)              # 数组第二个元素设置为 13; array[1] = $1 = 13
              #该数组中每个元素地址相距长度就是自身数据类型长度,即4字节, 所以对于array+4就是array[1]
              li $t1, -7                  #   $t1 = -7
              sw $t1, 8($t0)              #  第三个元素设置为 -7;  
#array+8 = (address[array[0])+4)+ 4 = address(array[1]) + 4 = address(array[2])
              done
算术指令
  • 最多使用3个参数
  • 所有操作数都是寄存器; 不能有内存地址的存在
  • 操作数大小是字 ( 4个字节 ), 32位 = 4 * 8 bit = 4bytes = 1 word
add    $t0,$t1,$t2   #  $t0 = $t1 + $t2;添加为带符号(2 的补码)整数
sub    $t2,$t3,$t4   #  $t2 = $t3 Ð $t4
addi   $t2,$t3, 5    #  $t2 = $t3 + 5;
addu   $t1,$t6,$t7   #  $t1 = $t6 + $t7;跟无符号数那样相加
subu   $t1,$t6,$t7   #  $t1 = $t6 - $t7;跟无符号数那样相减
mult   $t3,$t4       # 运算结果存储在hi,lo(hi高位数据, lo地位数据)
div    $t5,$t6       #  Lo = $t5 / $t6   (整数商)
                     #  Hi = $t5 mod $t6   (求余数)
                     #商数存放在 lo, 余数存放在 hi
mfhi   $t0           #  把特殊寄存器 Hi 的值移动到 $t0 : $t0 = Hi
mflo   $t1           #  把特殊寄存器 Lo 的值移动到 $t1:   $t1 = Lo
#不能直接获取 hi 或 lo中的值, 需要mfhi, mflo指令传值给寄存器
move   $t2,$t3       #  $t2 = $t3
流程控制

分支 ( if-else )

  • 条件分支的比较内置于指令中
              b target #无条件分支,直接到程序标签目标
              beq $t0, $t1, target #if $t0 = $ t1, 就跳到目标
              blt $t0, $t1, target #if $t0 <$ t1, 就跳到目标
              ble $t0, $t1, target #if $t0 <= $ t1, 就跳到目标
              bgt $t0, $t1, target #if $t0  $ t1, 就跳到目标
              bge $t0, $t1, target #if $t0  = $ t1, 就跳到目标
              bne    $t0, $t1, target #if  $t0 < $t1, 就跳到目标

跳转 ( while, for, goto )

 j     target #看到就跳, 不用考虑任何条件
 jr    $t3    #类似相对寻址,跳到该寄存器给出的地址处

子程序调用

子程序调用:“ 跳转和链接 ” 指令

       jal sub_label #“跳转和链接”
  • 将当前的程序计数器保存到 $ra

  • 跳转到 sub_label 的程序语句

    子程序返回:“跳转寄存器”指令

       jr $ra       #“跳转寄存器”
  • 跳转到$ ra中的地址(由jal指令存储)

    注意:寄存地址存储在寄存器 $ra 中; 如果子例程将调用其他子例程,或者是递归的,则返回地址应该从 $ra 复制到栈以保留它,因为 jal 总是将返回地址放在该寄存器中,因此将覆盖之前的值

系统调用和 I / O( 针对 SPIM 模拟器 )
  • 通过系统调用实现从输入/输出窗口读取或打印值或字符串,并指示程序结束

  • syscall

  • 首先在寄存器 $v0$a0 - $a1中提供适当的值

  • 寄存器 $v0 中存储返回的结果值( 如果有的话 )

    下表列出了可能的 系统调用 服务。

Service 服务Code in $v0 对应功能的调用码Arguments 所需参数Results 返回值
print 一个整型数$v0 = 1$a0 = 要打印的整型数
print 一个浮点数$v0 = 2$f12 = 要打印的浮点数
print 双精度数$v0 = 3$f12 = 要打印的双精度数
print 字符串$v0 = 4$a0 = 要打印的字符串的地址
读取 ( read ) 整型数$v0 = 5$v0 = 读取的整型数
读取 ( read ) 浮点数$v0 = 6$v0 = 读取的浮点数
读取 ( read ) 双精度数$v0= 7$v0 = 读取的双精度
读取 ( read ) 字符串$v0 = 8将读取的字符串地址赋值给 $a0; 将读取的字符串长度赋值给 $a1
这个应该和 C 语言的 sbrk() 函数一样$v0 = 9需要分配的空间大小(单位目测是字节 bytes)将分配好的空间首地址给 $v0
exit$v0 =10这个还要说吗……= _ =
    • print_stringprint 字符串 服务期望启动以 null 结尾的字符串。指令.asciiz 创建一个以 null 结尾的字符串。
  • read_intread_floatread_double 服务读取整行输入,包括换行符\n

    • read_string
      

      服务与 UNIX 库例程 fgets 具有相同的语义。

      • 它将最多 n-1 个字符读入缓冲区,并以空字符终止字符串。
      • 如果当前行中少于 n-1 个字符,则它会读取并包含换行符,并使用空字符终止该字符串。
      • 就是输入过长就截取,过短就这样,最后都要加一个终止符。
    • sbrk 服务将地址返回到包含 n 个附加字节的内存块。这将用于动态内存分配。

    • 退出服务使程序停止运行

  • 例子 : 打印一个存储在 $2 的整型数

 li $v0, 1    #声明需要调用的操作代码为 1 ( print_int ), 然后赋值给 $v0
 move $a0, $t2 #把这个要打印的整型数赋值给 $a0
 syscall #让操作系统执行我们的操作
  • 例子 : 读取一个数,并且存储到内存中的 int_value 变量中

    li $v0, 5 #声明需要调用的操作代码为 5 ( read_int ), 然后赋值给 $v0
    syscall #让操作系统执行我们的操作, 然后 $v0 = 5
    sw    $v0, int_value #通过写入(store_word)指令 将 $v0 的值(5)存入内存中
    
  • 例子 : 打印一个字符串 ( 这是完整的,其实上面例子都可以直接替换 main: 部分,都能直接运行 )

              .data
 string1             .asciiz       "Print this.\n"             # 字符串变量声明
                                          # .asciiz 指令使字符串 null 终止
              .text
 main: li     $v0, 4               # 将适当的系统调用代码加载到寄存器 $v0 中
                                   # 打印字符串, 赋值对应的操作代码 $v0 = 4
              la     $a0, string1  # 将要打印的字符串地址赋值  $a0 = address(string1)
              syscall              # 让操作系统执行打印操作
 要指示程序结束, 应该退出系统调用, 所以最后一行代码应该是这个 :
              li     $v0, 10     #对着上面的表, 不用说了吧
              syscall              # 让操作系统结束这一切吧 !
补充 : MIPS 指令格式
  • R格式
655556
oprsrtrdshamtfunct

用处:寄存器 - 寄存器 ALU 操作读写专用寄存器

  • I格式
65516
oprsrt立即数操作

用处:加载/存储 字节,半字,字,双字条件分支,跳转,跳转并链接寄存器

  • J格式
626
op跳转地址

用处:跳转,跳转并链接陷阱和从异常中返回

各字段含义: op : 指令基本操作,称为操作码。 rs : 第一个源操作数寄存器。 rt : 第二个源操作数寄存器。 rd : 存放操作结果的目的操作数。 shamt : 位移量; funct : 函数,这个字段选择 op 操作的某个特定变体。

例:

add $t0,$s0,$s1

表示$t0=$s0+$s1,即 16 号寄存器( s0 ) 的内容和 17 号寄存器 ( s1 ) 的内容相加,结果放到 8 号寄存器 ( t0 )。指令各字段的十进制表示为:

016178032

op = 0 和 funct = 32 表示这是加法,16 = $s0 表示第一个源操作数 ( rs ) 在 16 号寄存器里,

17 = $s1 表示第二个源操作数 ( rt ) 在 17 号寄存器里,8 = $t0 表示目的操作数 ( rd ) 在 8 号寄存器里。把各字段写成二进制,为:

00000010000100010100000000100000

这就是上述指令的机器码( machine code ), 可以看出是很有规则性的。

补充 : MIPS 常用指令集

lb/lh/lw : 从存储器中读取一个 byte / half word / word 的数据到寄存器中.

lb $1, 0($2)sb/sh/sw : 把一个 byte / half word / word 的数据从寄存器存储到存储器中.

sb $1, 0($2)add/addu : 把两个定点寄存器的内容相加

add $1,$2,$3($1=$2+$3); u 为不带符号加

addi/addiu : 把一个寄存器的内容加上一个立即数

add $1,$2,#3($1=$2+3); u 为不带符号加sub/subu :把两个定点寄存器的内容相减div/divu : 两个定点寄存器的内容相除mul/mulu : 两个定点寄存器的内容相乘and/andi : 与运算,两个寄存器中的内容相与

and $1,$2,$3($1=$2 & $3);i为立即数。or/ori : 或运算。xor/xori : 异或运算。beq/beqz/benz/bne : 条件转移 eq 相等,z 零,ne 不等j/jr/jal/jalr : j 直接跳转;jr 使用寄存器跳转lui : 把一个 16 位的立即数填入到寄存器的高 16 位,低 16 位补零sll/srl : 逻辑 左移 / 右移

sll $1,$2,#2slt/slti/sltui : 如果 $2 的值小于 $3,那么设置 $1 的值为 1,否则设置 $1 的值为 0

slt $1,$2,$3mov/movz/movn : 复制,n 为负,z 为零

mov $1,$2; movz $1,$2,$3 ( $3 为零则复制 $2$1 )trap : 根据地址向量转入管态eret : 从异常中返回到用户态

参考资料

[参考资料](http://logos.cs.uic.edu/366/notes/mips quick tutorial.htm)

  • 2.3.1 GDB
    • [gdb 的组成架构](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#gdb 的组成架构)
    • gdb 基本工作原理
      • [gdb 的三种调试方式](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#gdb 的三种调试方式)
      • 断点的实现
    • gdb 基本操作
      • [break — b](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#break — b)
      • info
      • [disable — dis](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#disable — dis)
      • enable
      • clear
      • [delete — d](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#delete — d)
      • tbreak
      • watch
      • [step — s](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#step — s)
      • reverse-step
      • [next — n](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#next — n)
      • reverse-next
      • return
      • [finish — fin](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#finish — fin)
      • [until — u](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#until — u)
      • [continue — c](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#continue — c)
      • [print — p](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#print — p)
      • x
      • display
      • [disassemble — disas](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#disassemble — disas)
      • undisplay
      • [disable display](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#disable display)
      • [enable display](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#enable display)
      • [help — h](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#help — h)
      • attach
      • [run — r](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#run — r)
      • [backtrace — bt](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#backtrace — bt)
      • ptype
      • [set follow-fork-mode](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#set follow-fork-mode)
      • [thread apply all bt](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#thread apply all bt)
      • generate-core-file
      • [directory — dir](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#directory — dir)
    • gdb-peda
      • 安装
      • peda命令
      • [使用 PEDA 和 Python 编写 gdb 脚本](https://www.bookstack.cn/read/CTF-All-In-One/doc-2.3.1_gdb.md#使用 PEDA 和 Python 编写 gdb 脚本)
      • 更多资料
    • GEF/pwndbg
    • 参考资料

2.3.1 GDB

gdb 基本工作原理

gdb 通过系统调用 ptrace 来接管一个进程的执行。ptrace 系统调用提供了一种方法使得父进程可以观察和控制其它进程的执行,检查和改变其核心映像以及寄存器。它主要用来实现断点调试和系统调用跟踪。ptrace 系统调用的原型如下:

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
  • pid_t pid:指示 ptrace 要跟踪的进程。

  • void *addr:指示要监控的内存地址。

  • void *data:存放读取出的或者要写入的数据。

  • enum __ptrace_request request

    :决定了系统调用的功能,几个主要的选项:

    • PTRACE_TRACEME:表示此进程将被父进程跟踪,任何信号(除了 SIGKILL)都会暂停子进程,接着阻塞于 wait() 等待的父进程被唤醒。子进程内部对 exec() 的调用将发出 SIGTRAP 信号,这可以让父进程在子进程新程序开始运行之前就完全控制它。
    • PTRACE_ATTACH:attach 到一个指定的进程,使其成为当前进程跟踪的子进程,而子进程的行为等同于它进行了一次 PTRACE_TRACEME 操作。但需要注意的是,虽然当前进程成为被跟踪进程的父进程,但是子进程使用 getppid() 的到的仍将是其原始父进程的 pid。
    • PTRACE_CONT:继续运行之前停止的子进程。可同时向子进程交付指定的信号。

gdb 的三种调试方式

  • 运行并调试一个新进程

    • 运行 gdb,通过命令行或 file 命令指定目标程序。

    • 输入

      run
      

      命令, gdb 执行下面的操作:

      • 通过 fork() 系统调用创建一个新进程
      • 在新创建的子进程中执行操作:ptrace(PTRACE_TRACEME, 0, 0, 0)
      • 在子进程中通过 execv() 系统调用加载用户指定的可执行文件
  • attach 并调试一个已经运行的进程

    • 用户确定需要进行调试的进程 PID
    • 运行 gdb,输入 attach <pid>,gdb 将对指定进程执行操作:ptrace(PTRACE_ATTACH, pid, 0, 0)
  • 远程调试目标机上新创建的进程

    • gdb 运行在调试机上,gdbserver 运行在目标机上,两者之间的通信数据格式由 gdb 远程串行协议(Remote Serial Protocol)定义
    • RSP 协议数据的基本格式为: $..........#xx
    • gdbserver 的启动方式相当于运行并调试一个新创建的进程

注意,在你将 gdb attach 到一个进程时,可能会出现这样的问题:

gdb-peda$ attach 9091
Attaching to process 9091
ptrace: Operation not permitted.

这是因为开启了内核参数 ptrace_scope

$ cat /proc/sys/kernel/yama/ptrace_scope
1

1 表示 True,此时普通用户进程是不能对其他进程进行 attach 操作的,当然你可以用 root 权限启动 gdb,但最好的办法还是关掉它:

# echo 0 > /proc/sys/kernel/yama/ptrace_scope

断点的实现

断点的功能是通过内核信号实现的,在 x86 架构上,内核向某个地址打入断点,实际上就是往该地址写入断点指令 INT 3,即 0xCC。目标程序运行到这条指令之后会触发 SIGTRAP 信号,gdb 捕获这个信号,并根据目标程序当前停止的位置查询 gdb 维护的断点链表,若发现在该地址确实存在断点,则可判定为断点命中。

gdb 基本操作

使用 -tui 选项可以将代码显示在一个漂亮的交互式窗口中。

break — b

  • break 当不带参数时,在所选栈帧中执行的下一条指令处设置断点。
  • break <function> 在函数体入口处打断点。
  • break <line> 在当前源码文件指定行的开始处打断点。
  • break -N break +N 在当前源码行前面或后面的 N 行开始处打断点,N 为正整数。
  • break <filename:line> 在源码文件 filenameline 行处打断点。
  • break <filename:function> 在源码文件 filenamefunction 函数入口处打断点。
  • break <address> 在程序指令的地址处打断点。
  • break ... if <cond> 设置条件断点,... 代表上述参数之一(或无参数),cond 为条件表达式,仅在 cond 值非零时停住程序。

info

  • info breakpoints -- i b
    

    查看断点,观察点和捕获点的列表。

    • info breakpoints [list…]
    • info break [list…]
    • list… 用来指定若干个断点的编号(可省略),可以是 21-32 5 等。
  • info display 打印自动显示的表达式列表,每个表达式都带有项目编号,但不显示其值。

  • info reg 显示当前寄存器信息。

  • info threads 打印出所有线程的信息,包含 Thread ID、Target ID 和 Frame。

  • info frame 打印出指定栈帧的详细信息。

  • info proc 查看 proc 里的进程信息。

disable — dis

禁用断点,参数使用空格分隔。不带参数时禁用所有断点。

  • disable [breakpoints] [list…] breakpointsdisable 的子命令(可省略),list…info breakpoints 中的描述。

enable

启用断点,参数使用空格分隔。不带参数时启用所有断点。

  • enable [breakpoints] [list…] 启用指定的断点(或所有定义的断点)。
  • enable [breakpoints] once list… 临时启用指定的断点。GDB 在停止您的程序后立即禁用这些断点。
  • enable [breakpoints] delete list… 使指定的断点启用一次,然后删除。一旦您的程序停止,GDB 就会删除这些断点。等效于用 tbreak 设置的断点。

breakpointsdisable 中的描述。

clear

在指定行或函数处清除断点。参数可以是行号,函数名称或 * 跟一个地址。

  • clear 当不带参数时,清除所选栈帧在执行的源码行中的所有断点。
  • clear <function>, clear <filename:function> 删除在命名函数的入口处设置的任何断点。
  • clear <line>, clear <filename:line> 删除在指定的文件指定的行号的代码中设置的任何断点。
  • clear <address> 清除指定程序指令的地址处的断点。

delete — d

删除断点。参数使用空格分隔。不带参数时删除所有断点。

  • delete [breakpoints] [list…]

tbreak

设置临时断点。参数形式同 break 一样。当第一次命中时被删除。

watch

为表达式设置观察点。每当一个表达式的值改变时,观察点就会停止执行您的程序。

  • watch [-l|-location] <expr> 如果给出了 -l 或者 -location,则它会对 expr 求值并观察它所指向的内存。

另外 rwatch 表示在访问时停止,awatch 表示在访问和改变时都停止。

step — s

单步执行程序,直到到达不同的源码行。

  • step [N] 参数 N 表示执行 N 次(或由于另一个原因直到程序停止)。

reverse-step

反向步进程序,直到到达另一个源码行的开头。

  • reverse-step [N] 参数 N 表示执行 N 次(或由于另一个原因直到程序停止)。

next — n

单步执行程序,执行完子程序调用。

  • next [N]

step 不同,如果当前的源代码行调用子程序,则此命令不会进入子程序,而是继续执行,将其视为单个源代码行。

reverse-next

反向步进程序,执行完子程序调用。

  • reverse-next [N]

如果要执行的源代码行调用子程序,则此命令不会进入子程序,调用被视为一个指令。

return

您可以使用 return 命令取消函数调用的执行。如果你给出一个表达式参数,它的值被用作函数的返回值。

  • return <expression>expression 的值作为函数的返回值并使函数直接返回。

finish — fin

执行直到选定的栈帧返回。

  • finish

until — u

执行程序直到大于当前栈帧或当前栈帧中的指定位置(与 break 命令相同的参数)的源码行。此命令常用于通过一个循环,以避免单步执行。

  • until <location> 继续运行程序,直到达到指定的位置,或者当前栈帧返回。

continue — c

在信号或断点之后,继续运行被调试的程序。

  • continue [N]

如果从断点开始,可以使用数字 N 作为参数,这意味着将该断点的忽略计数设置为 N - 1(以便断点在第 N 次到达之前不会中断)。

print — p

求表达式 expr 的值并打印。可访问的变量是所选栈帧的词法环境,以及范围为全局或整个文件的所有变量。

  • print [expr]
  • print /f [expr] 通过指定 /f 来选择不同的打印格式,其中 f 是一个指定格式的字母

x

检查内存。

  • x/nfu <addr>

  • x <addr>
    
    • n, f, 和 u 都是可选参数,用于指定要显示的内存以及如何格式化。
    • addr 是要开始显示内存的地址的表达式。
    • n 重复次数(默认值是 1),指定要显示多少个单位(由 u 指定)的内存值。
    • f 显示格式(初始默认值是 x),显示格式是 print('x','d','u','o','t','a','c','f','s') 使用的格式之一,再加 i(机器指令)。
    • u 单位大小,b 表示单字节,h 表示双字节,w 表示四字节,g 表示八字节。

display

每次程序停止时打印表达式 expr 的值。

  • display <expr>
  • display/fmt <expr>
  • display/fmt <addr>

fmt 用于指定显示格式。对于格式 is,或者包括单位大小或单位数量,将表达式 addr 添加为每次程序停止时要检查的内存地址。

disassemble — disas

反汇编命令。

  • disas <func> 反汇编指定函数
  • disas <addr> 反汇编某地址所在函数
  • disas <begin_addr> <end_addr> 反汇编从开始地址到结束地址的部分

undisplay

取消某些表达式在程序停止时自动显示。参数是表达式的编号(使用 info display 查询编号)。不带参数表示取消所有自动显示表达式。

disable display

禁用某些表达式在程序停止时自动显示。禁用的显示项目被再次启用。参数是表达式的编号(使用 info display 查询编号)。不带参数表示禁用所有自动显示表达式。

enable display

启用某些表达式在程序停止时自动显示。参数是重新显示的表达式的编号(使用 info display 查询编号)。不带参数表示启用所有自动显示表达式。

help — h

打印命令列表。

  • help <class> 您可以获取该类中各个命令的列表。
  • help <command> 显示如何使用该命令的简述。

attach

挂接到 GDB 之外的进程或文件。将进程 ID 或设备文件作为参数。

  • attach <process-id>

run — r

启动被调试的程序。可以直接指定参数,也可以用 set args 设置(启动所需的)参数。还允许使用 >, <, 或 >> 进行输入和输出重定向。

甚至可以运行一个脚本,如:

run `python2 -c 'print "A"*100'`

backtrace — bt

打印整个栈的回溯。

  • bt 打印整个栈的回溯,每个栈帧一行。
  • bt n 类似于上,但只打印最内层的 n 个栈帧。
  • bt -n 类似于上,但只打印最外层的 n 个栈帧。
  • bt full n 类似于 bt n,还打印局部变量的值。

注意:使用 gdb 调试时,会自动关闭 ASLR,所以可能每次看到的栈地址都不变。

ptype

打印类型 TYPE 的定义。

  • ptype[/FLAGS] TYPE-NAME | EXPRESSION

参数可以是由 typedef 定义的类型名, 或者 struct STRUCT-TAG 或者 class CLASS-NAME 或者 union UNION-TAG 或者 enum ENUM-TAG

set follow-fork-mode

当程序 fork 出一个子进程的时候,gdb 默认会追踪父进程(set follow-fork-mode parent),但也可以使用命令 set follow-fork-mode child 让其追踪子进程。

另外,如果想要同时追踪父进程和子进程,可以使用命令 set detach-on-fork off(默认为on),这样就可以同时调试父子进程,在调试其中一个进程时,另一个进程被挂起。如果想让父子进程同时运行,可以使用 set schedule-multiple on(默认为off)。

但如果程序是使用 exec 来启动了一个新的程序,可以使用 set follow-exec-mode new(默认为same) 来新建一个 inferior 给新程序,而父进程的 inferior 仍然保留。

thread apply all bt

打印出所有线程的堆栈信息。

generate-core-file

将调试中的进程生成内核转储文件。

directory — dir

设置查找源文件的路径。

或者使用 gdb 的 -d 参数,例如:gdb a.out -d /search/code/

gdb-peda

当 gdb 启动时,它会在当前用户的主目录中寻找一个名为 .gdbinit 的文件;如果该文件存在,则 gdb 就执行该文件中的所有命令。通常,该文件用于简单的配置命令。但是 .gdbinit 的配置十分繁琐,因此对 gdb 的扩展通常用插件的方式来实现,通过 python 的脚本可以很方便的实现需要的功能。

PEDA(Python Exploit Development Assistance for GDB)是一个强大的 gdb 插件。它提供了高亮显示反汇编代码、寄存器、内存信息等人性化的功能。同时,PEDA 还有一些实用的新命令,比如 checksec 可以查看程序开启了哪些安全机制等等。

安装

安装 peda 需要的软件包:

$ sudo apt-get install nasm micro-inetd
$ sudo apt-get install libc6-dbg vim ssh

安装 peda:

$ git clone https://github.com/longld/peda.git ~/peda
$ echo "source ~/peda/peda.py" >> ~/.gdbinit
$ echo "DONE! debug your program with gdb and enjoy"

如果系统为 Arch Linux,则可以直接安装:

$ yaourt -S peda

peda命令

  • aslr — 显示/设置 gdb 的 ASLR

  • asmsearch
    

    — Search for ASM instructions in memory

    • asmsearch "int 0x80"
    • asmsearch "add esp, ?" libc
  • assemble
    

    — On the fly assemble and execute instructions using NASM

    • assemble

    • assemble $pc
      > mov al, 0xb
      > int 0x80
      > end
      
  • checksec — 检查二进制文件的安全选项

  • cmpmem
    

    — Compare content of a memory region with a file

    • cmpmem 0x08049000 0x0804a000 data.mem
  • context
    

    — Display various information of current execution context

    • context_code — Display nearby disassembly at $PC of current execution context

    • context_register — Display register information of current execution context

    • context_stack
      

      — Display stack of current execution context

      • context reg
      • context code
      • context stack
  • crashdump — Display crashdump info and save to file

  • deactive
    

    — Bypass a function by ignoring its execution (eg sleep/alarm)

    • deactive setresuid
    • deactive chdir
  • distance — Calculate distance between two addresses

  • dumpargs — 在调用指令停止时显示传递给函数的参数

  • dumpmem
    

    — Dump content of a memory region to raw binary file

    • dumpmem libc.mem libc
  • dumprop

    — 在特定的内存范围显示 ROP gadgets

    • dumprop
    • dumprop binary "pop"
  • eflags — Display/set/clear/toggle value of eflags register

  • elfheader

    — 获取正在调试的 ELF 文件的头信息

    • elfheader
    • elfheader .got
  • elfsymbol

    — 从 ELF 文件中获取没有调试信息的符号信息

    • elfsymbol
    • elfsymbol printf
  • gennop
    

    — Generate abitrary length NOP sled using given characters

    • gennop 500
    • gennop 500 "\x90"
  • getfile — Get exec filename of current debugged process

  • getpid — Get PID of current debugged process

  • goto — Continue execution at an address

  • help — Print the usage manual for PEDA commands

  • hexdump
    

    — Display hex/ascii dump of data in memory

    • hexdump $sp 64
    • hexdump $sp /20
  • hexprint
    

    — Display hexified of data in memory

    • hexprint $sp 64
    • hexprint $sp /20
  • jmpcall
    

    — Search for JMP/CALL instructions in memory

    • jmpcall
    • jmpcall eax
    • jmpcall esp libc
  • loadmem
    

    — Load contents of a raw binary file to memory

    • loadmem stack.mem 0xbffdf000
  • lookup

    — 搜索属于内存范围的地址的所有地址/引用

    • lookup address stack libc
    • lookup pointer stack ld-2
  • nearpc
    

    — Disassemble instructions nearby current PC or given address

    • nearpc 20
    • nearpc 0x08048484
  • nextcall
    

    — Step until next ‘call’ instruction in specific memory range

    • nextcall cpy
  • nextjmp
    

    — Step until next ‘j*’ instruction in specific memory range

    • nextjmp
  • nxtest — Perform real NX test to see if it is enabled/supported by OS

  • patch

    — 使用字符串/十六进制字符串/整形数

    • patch $esp 0xdeadbeef
    • patch $eax "the long string"
    • patch (multiple lines)
  • pattern

    — 生成,搜索或写入循环 pattern 到内存

    • pattern_arg — Set argument list with cyclic pattern

    • pattern_create — Generate a cyclic pattern

    • pattern_env — Set environment variable with a cyclic pattern

    • pattern_offset — Search for offset of a value in cyclic pattern

    • pattern_patch — Write a cyclic pattern to memory

    • pattern_search
      

      — Search a cyclic pattern in registers and memory

      • pattern create 2000
      • pattern create 2000 input
      • pattern offset $pc
      • pattern search
      • pattern patch 0xdeadbeef 100
  • payload
    

    — Generate various type of ROP payload using ret2plt

    • payload copybytes
    • payload copybytes target "/bin/sh"
    • payload copybytes 0x0804a010 offset
  • pdisass
    

    — Format output of gdb disassemble command with colors

    • pdisass $pc /20
  • pltbreak
    

    — Set breakpoint at PLT functions match name regex

    • pltbreak cpy
  • procinfo

    — 显示调试进程的 /proc/pid/

    • procinfo
    • procinfo fd
  • profile — Simple profiling to count executed instructions in the program

  • pyhelp
    

    — Wrapper for python built-in help

    • pyhelp peda
    • pyhelp hex2str
  • pshow

    — 显示各种 PEDA 选项和其他设置

    • pshow
    • pshow option context
  • pset

    — 设置各种 PEDA 选项和其他设置

    • pset arg '"A"*200'
    • pset arg 'cyclic_pattern(200)'
    • pset env EGG 'cyclic_pattern(200)'
    • pset option context "code,stack"
    • pset option badchars "\r\n"
  • readelf

    — 获取 ELF 的文件头信息

    • readelf libc .text
  • refsearch
    

    — Search for all references to a value in memory ranges

    • refsearch "/bin/sh"
    • refsearch 0xdeadbeef
  • reload — Reload PEDA sources, keep current options untouch

  • ropgadget

    — 获取二进制或库的常见 ROP gadgets

    • ropgadget
    • ropgadget libc
  • ropsearch

    — 搜索内存中的 ROP gadgets

    • ropsearch "pop eax"
    • ropsearch "xchg eax, esp" libc
  • searchmem|find

    — 搜索内存中的 pattern; 支持正则表达式搜索

    • find "/bin/sh" libc
    • find 0xdeadbeef all
    • find "..\x04\x08" 0x08048000 0x08049000
  • searchmem — Search for a pattern in memory; support regex search

  • session — Save/restore a working gdb session to file as a script

  • set
    

    — Set various PEDA options and other settings

    • set exec-wrapper ./exploit.py
  • sgrep — Search for full strings contain the given pattern

  • shellcode

    — 生成或下载常见的 shellcode

    • shellcode x86/linux exec
  • show — Show various PEDA options and other settings

  • skeleton

    — 生成 python exploit 代码模板

    • skeleton argv exploit.py
  • skipi — Skip execution of next count instructions

  • snapshot
    

    — Save/restore process’s snapshot to/from file

    • snapshot save
    • snapshot restore
  • start — Start debugged program and stop at most convenient entry

  • stepuntil
    

    — Step until a desired instruction in specific memory range

    • stepuntil cmp
    • stepuntil xor
  • strings
    

    — Display printable strings in memory

    • strings
    • strings binary 4
  • substr — Search for substrings of a given string/number in memory

  • telescope
    

    — Display memory content at an address with smart dereferences

    • telescope 40
    • telescope 0xb7d88000 40
  • tracecall
    

    — Trace function calls made by the program

    • tracecall
    • tracecall "cpy,printf"
    • tracecall "-puts,fflush"
  • traceinst
    

    — Trace specific instructions executed by the program

    • traceinst 20
    • traceinst "cmp,xor"
  • unptrace
    

    — Disable anti-ptrace detection

    • unptrace
  • utils — Miscelaneous utilities from utils module

  • vmmap

    — 在调试过程中获取段的虚拟映射地址范围

    • cmmap
    • vmmap binary / libc
    • vmmap 0xb7d88000
  • waitfor
    

    — Try to attach to new forked process; mimic “attach -waitfor”

    • waitfor
    • waitfor myprog -c
  • xinfo
    

    — Display detail information of address/registers

    • xinfo register eax
    • xinfo 0xb7d88000
  • xormem

    — 用一个 key 来对一个内存区域执行 XOR 操作

    • xormem 0x08049000 0x0804a000 “thekey”
  • xprint — Extra support to GDB’s print command

  • xrefs — Search for all call/data access references to a function/variable

  • xuntil — Continue execution until an address or function

使用 PEDA 和 Python 编写 gdb 脚本

  • 全局类

    • pedacmd
      

      • 交互式命令
      • 没有返回值
      • 例如:pedacmd.context_register()
    • peda
      

      • 与 gdb 交互的后端功能
      • 有返回值
      • 例如:peda.getreg("eax")
  • 小工具

    • 例如:to_int()format_address()
    • 获得帮助
      • pyhelp peda
      • pyhelp hex2str
  • 单行/交互式使用

    • gdb-peda$ python print peda.get_vmmap()

    • gdb-peda$ python
      > status = peda.get_status()
      > while status == "BREAKPOINT":
      >    peda.execute("continue")
      > end
      
  • 外部脚本

    • # myscript.py
      def myrun(size):
          argv = cyclic_pattern(size)
          peda.execute("set arg %s" % argv)
          peda.execute("run")
      
      gdb-peda$ source myscript.py
      gdb-peda$ python myrun(100)
      

更多资料

http://ropshell.com/peda/

GEF/pwndbg

除了 PEDA 外还有一些优秀的 gdb 增强工具,特别是增加了一些查看堆的命令,可以看情况选用。

  • GEF - Multi-Architecture GDB Enhanced Features for Exploiters & Reverse-Engineers
  • pwndbg - Exploit Development and Reverse Engineering with GDB Made Easy

参考资料

4.2 Linux 命令行技巧

通配符

  • *
    

    :匹配任意字符

    • ls test*
  • ?
    

    :匹配任意单个字符

    • ls test?
  • [...]
    

    :匹配括号内的任意单个字符

    • ls test[123]
  • [!...]
    

    :匹配除括号内字符以外的单个字符

    • ls test[!123]

重定向输入字符

有时候我们需要在 shell 里输入键盘上没有对应的字符,如 0x1F,就需要使用重定向输入。下面是一个例子:

#include<stdio.h>
#include<string.h>
void main() {
    char data[8];
    char str[8];
    printf("请输入十六进制为 0x1f 的字符: ");
    sprintf(str, "%c", 31);
    scanf("%s", data);
    if (!strcmp((const char *)data, (const char *)str)) {
        printf("correct\n");
    } else {
        printf("wrong\n");
    }
}
$ gcc test.c
$ ./a.out
请输入十六进制为 0x1f 的字符: 0x1f
wrong
$ echo -e "\x1f"
$ echo -e "\x1f" | ./a.out
请输入十六进制为 0x1f 的字符: correct

从可执行文件中提取 shellcode

for i in `objdump -d print_flag | tr '\t' ' ' | tr ' ' '\n' | egrep '^[0-9a-f]{2}$' ` ; do echo -n "\x$i" ; done

注意:在 objdump 中空字节可能会被删除。

查看进程虚拟地址空间

有时我们需要知道一个进程的虚拟地址空间是如何使用的,以确定栈是否是可执行的。

$ cat /proc/<PID>/maps

下面我们分别来看看可执行栈和不可执行栈的不同:

$ cat hello.c
#include <stdio.h>
void main()
{
    char buf[128];
    scanf("hello, world: %s\n", buf);
}
$ gcc hello.c -o a.out1
$ ./a.out1 &
[1] 7403
$ cat /proc/7403/maps
555555554000-555555555000 r-xp 00000000 08:01 26389924                   /home/firmy/a.out1
555555754000-555555755000 r--p 00000000 08:01 26389924                   /home/firmy/a.out1
555555755000-555555756000 rw-p 00001000 08:01 26389924                   /home/firmy/a.out1
555555756000-555555777000 rw-p 00000000 00:00 0                          [heap]
7ffff7a33000-7ffff7bd0000 r-xp 00000000 08:01 21372436                   /usr/lib/libc-2.25.so
7ffff7bd0000-7ffff7dcf000 ---p 0019d000 08:01 21372436                   /usr/lib/libc-2.25.so
7ffff7dcf000-7ffff7dd3000 r--p 0019c000 08:01 21372436                   /usr/lib/libc-2.25.so
7ffff7dd3000-7ffff7dd5000 rw-p 001a0000 08:01 21372436                   /usr/lib/libc-2.25.so
7ffff7dd5000-7ffff7dd9000 rw-p 00000000 00:00 0
7ffff7dd9000-7ffff7dfc000 r-xp 00000000 08:01 21372338                   /usr/lib/ld-2.25.so
7ffff7fbc000-7ffff7fbe000 rw-p 00000000 00:00 0
7ffff7ff8000-7ffff7ffa000 r--p 00000000 00:00 0                          [vvar]
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0                          [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 00023000 08:01 21372338                   /usr/lib/ld-2.25.so
7ffff7ffd000-7ffff7ffe000 rw-p 00024000 08:01 21372338                   /usr/lib/ld-2.25.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
[1]+  Stopped                 ./a.out1
$ gcc -z execstack hello.c -o a.out2
$ ./a.out2 &
[2] 7467
[firmy@manjaro ~]$ cat /proc/7467/maps
555555554000-555555555000 r-xp 00000000 08:01 26366643                   /home/firmy/a.out2
555555754000-555555755000 r-xp 00000000 08:01 26366643                   /home/firmy/a.out2
555555755000-555555756000 rwxp 00001000 08:01 26366643                   /home/firmy/a.out2
555555756000-555555777000 rwxp 00000000 00:00 0                          [heap]
7ffff7a33000-7ffff7bd0000 r-xp 00000000 08:01 21372436                   /usr/lib/libc-2.25.so
7ffff7bd0000-7ffff7dcf000 ---p 0019d000 08:01 21372436                   /usr/lib/libc-2.25.so
7ffff7dcf000-7ffff7dd3000 r-xp 0019c000 08:01 21372436                   /usr/lib/libc-2.25.so
7ffff7dd3000-7ffff7dd5000 rwxp 001a0000 08:01 21372436                   /usr/lib/libc-2.25.so
7ffff7dd5000-7ffff7dd9000 rwxp 00000000 00:00 0
7ffff7dd9000-7ffff7dfc000 r-xp 00000000 08:01 21372338                   /usr/lib/ld-2.25.so
7ffff7fbc000-7ffff7fbe000 rwxp 00000000 00:00 0
7ffff7ff8000-7ffff7ffa000 r--p 00000000 00:00 0                          [vvar]
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0                          [vdso]
7ffff7ffc000-7ffff7ffd000 r-xp 00023000 08:01 21372338                   /usr/lib/ld-2.25.so
7ffff7ffd000-7ffff7ffe000 rwxp 00024000 08:01 21372338                   /usr/lib/ld-2.25.so
7ffff7ffe000-7ffff7fff000 rwxp 00000000 00:00 0
7ffffffde000-7ffffffff000 rwxp 00000000 00:00 0                          [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
[2]+  Stopped                 ./a.out2

当使用 -z execstack 参数进行编译时,会关闭 Stack Protector。我们可以看到在 a.out1 中的 stackrw 的,而 a.out2 中则是 rwx 的。

maps 文件有 6 列,分别为:

  • 地址:库在进程里地址范围
  • 权限:虚拟内存的权限,r=读,w=写,x=执行,s=共享,p=私有
  • 偏移量:库在进程里地址偏移量
  • 设备:映像文件的主设备号和次设备号,可以通过通过 cat /proc/devices 查看设备号对应的设备名
  • 节点:映像文件的节点号
  • 路径: 映像文件的路径,经常同一个地址有两个地址范围,那是因为一段是 r-xp 为只读的代码段,一段是 rwxp 为可读写的数据段

除了 /proc/<PID>/maps 之外,还有一些有用的设备和文件。

  • /proc/kcore 是 Linux 内核运行时的动态 core 文件。它是一个原始的内存转储,以 ELF core 文件的形式呈现,可以使用 GDB 来调试和分析内核。

  • /boot/System.map 是一个特定内核的内核符号表。它是你当前运行的内核的 System.map 的链接。

  • /proc/kallsymsSystem.map 很类似,但它在 /proc 目录下,所以是由内核维护的,并可以动态更新。

  • /proc/iomem/proc/<pid>/maps 类似,但它是用于系统内存的。如:

    # cat /proc/iomem | grep Kernel
    01000000-01622d91 : Kernel code
    01622d92-01b0ddff : Kernel data
    01c56000-01d57fff : Kernel bss
    

ASCII 表

ASCII 表将键盘上的所有字符映射到固定的数字。有时候我们可能需要查看这张表:

$ man ascii
Oct   Dec   Hex   Char                        Oct   Dec   Hex   Char
────────────────────────────────────────────────────────────────────────
000   0     00    NUL '\0' (null character)   100   64    40    @
001   1     01    SOH (start of heading)      101   65    41    A
002   2     02    STX (start of text)         102   66    42    B
003   3     03    ETX (end of text)           103   67    43    C
004   4     04    EOT (end of transmission)   104   68    44    D
005   5     05    ENQ (enquiry)               105   69    45    E
006   6     06    ACK (acknowledge)           106   70    46    F
007   7     07    BEL '\a' (bell)             107   71    47    G
010   8     08    BS  '\b' (backspace)        110   72    48    H
011   9     09    HT  '\t' (horizontal tab)   111   73    49    I
012   10    0A    LF  '\n' (new line)         112   74    4A    J
013   11    0B    VT  '\v' (vertical tab)     113   75    4B    K
014   12    0C    FF  '\f' (form feed)        114   76    4C    L
015   13    0D    CR  '\r' (carriage ret)     115   77    4D    M
016   14    0E    SO  (shift out)             116   78    4E    N
017   15    0F    SI  (shift in)              117   79    4F    O
020   16    10    DLE (data link escape)      120   80    50    P
021   17    11    DC1 (device control 1)      121   81    51    Q
022   18    12    DC2 (device control 2)      122   82    52    R
023   19    13    DC3 (device control 3)      123   83    53    S
024   20    14    DC4 (device control 4)      124   84    54    T
025   21    15    NAK (negative ack.)         125   85    55    U
026   22    16    SYN (synchronous idle)      126   86    56    V
027   23    17    ETB (end of trans. blk)     127   87    57    W
030   24    18    CAN (cancel)                130   88    58    X
031   25    19    EM  (end of medium)         131   89    59    Y
032   26    1A    SUB (substitute)            132   90    5A    Z
033   27    1B    ESC (escape)                133   91    5B    [
034   28    1C    FS  (file separator)        134   92    5C    \  '\\'
035   29    1D    GS  (group separator)       135   93    5D    ]
036   30    1E    RS  (record separator)      136   94    5E    ^
037   31    1F    US  (unit separator)        137   95    5F    _
040   32    20    SPACE                       140   96    60    `
041   33    21    !                           141   97    61    a
042   34    22    "                           142   98    62    b
043   35    23    #                           143   99    63    c
044   36    24    $                           144   100   64    d
045   37    25    %                           145   101   65    e
046   38    26    &                           146   102   66    f
047   39    27    '                           147   103   67    g
050   40    28    (                           150   104   68    h
051   41    29    )                           151   105   69    i
052   42    2A    *                           152   106   6A    j
053   43    2B    +                           153   107   6B    k
054   44    2C    ,                           154   108   6C    l
055   45    2D    -                           155   109   6D    m
056   46    2E    .                           156   110   6E    n
057   47    2F    /                           157   111   6F    o
060   48    30    0                           160   112   70    p
061   49    31    1                           161   113   71    q
062   50    32    2                           162   114   72    r
063   51    33    3                           163   115   73    s
064   52    34    4                           164   116   74    t
065   53    35    5                           165   117   75    u
066   54    36    6                           166   118   76    v
067   55    37    7                           167   119   77    w
070   56    38    8                           170   120   78    x
071   57    39    9                           171   121   79    y
072   58    3A    :                           172   122   7A    z
073   59    3B    ;                           173   123   7B    {
074   60    3C    <                           174   124   7C    |
075   61    3D    =                           175   125   7D    }
076   62    3E    >                           176   126   7E    ~
077   63    3F    ?                           177   127   7F    DEL
Tables
For convenience, below are more compact tables in hex and decimal.
   2 3 4 5 6 7       30 40 50 60 70 80 90 100 110 120
 -------------      ---------------------------------
0:   0 @ P ` p     0:    (  2  <  F  P  Z  d   n   x
1: ! 1 A Q a q     1:    )  3  =  G  Q  [  e   o   y
2: " 2 B R b r     2:    *  4  >  H  R  \  f   p   z
3: # 3 C S c s     3: !  +  5  ?  I  S  ]  g   q   {
4: $ 4 D T d t     4: "  ,  6  @  J  T  ^  h   r   |
5: % 5 E U e u     5: #  -  7  A  K  U  _  i   s   }
6: & 6 F V f v     6: $  .  8  B  L  V  `  j   t   ~
7: ' 7 G W g w     7: %  /  9  C  M  W  a  k   u  DEL
8: ( 8 H X h x     8: &  0  :  D  N  X  b  l   v
9: ) 9 I Y i y     9: '  1  ;  E  O  Y  c  m   w
A: * : J Z j z
B: + ; K [ k {
C: , < L \ l |
D: - = M ] m }
E: . > N ^ n ~
F: / ? O _ o DEL

Hex 转 Char:

$ echo -e '\x41\x42\x43\x44'
$ printf '\x41\x42\x43\x44'
$ python -c 'print(u"\x41\x42\x43\x44")'
$ perl -e 'print "\x41\x42\x43\x44";'

Char 转 Hex:

$ python -c 'print(b"ABCD".hex())'

nohup 和 &

nohup 运行命令可以使命令永久的执行下去,和 Shell 没有关系,而 & 表示设置此进程为后台进程。默认情况下,进程是前台进程,这时就把 Shell 给占据了,我们无法进行其他操作,如果我们希望其在后台运行,可以使用 & 达到这个目的。

该命令的一般形式为:

$ nohup <command> &

前后台进程切换

可以通过 bg(background)和 fg(foreground)命令进行前后台进程切换。

显示Linux中的任务列表及任务状态:

$ jobs -l
[1]+  9433 Stopped (tty input)     ./a.out

将进程放到后台运行:

$ bg 1

将后台进程放到前台运行:

$ fg 1

cat -

通常使用 cat 时后面都会跟一个文件名,但如果没有,或者只有一个 -,则表示从标准输入读取数据,它会保持标准输入开启,如:

$ cat -
hello world
hello world
^C

更进一步,如果你采用 cat file - 的用法,它会先输出 file 的内容,然后是标准输入,它将标准输入的数据复制到标准输出,并保持标准输入开启:

$ echo hello > text
$ cat text -
hello
world
world
^C

有时我们在向程序发送 paylaod 的时候,它执行完就直接退出了,并没有开启 shell,我们就可以利用上面的技巧:

$ cat payload | ./a.out
> Segmentation fault (core dumped)
$ cat payload - | ./a.out
whoami
firmy
^C
Segmentation fault (core dumped)

这样就得到了 shell。

5.2 动态二进制插桩

  • 5.2.1 Pin 动态二进制插桩
    • 插桩技术
    • [Pin 简介](https://www.bookstack.cn/read/CTF-All-In-One/doc-5.2.1_pin.md#Pin 简介)
    • [Pin 的基本结构和原理](https://www.bookstack.cn/read/CTF-All-In-One/doc-5.2.1_pin.md#Pin 的基本结构和原理)
    • [Pin 的基本用法](https://www.bookstack.cn/read/CTF-All-In-One/doc-5.2.1_pin.md#Pin 的基本用法)
    • [Pintool 示例分析](https://www.bookstack.cn/read/CTF-All-In-One/doc-5.2.1_pin.md#Pintool 示例分析)
    • Pintool 编写
    • Pin 在 CTF 中的应用
    • 扩展:Triton

5.2.1 Pin 动态二进制插桩

插桩技术

插桩技术是将额外的代码注入程序中以收集运行时的信息,可分为两种:

源代码插桩(Source Code Instrumentation(SCI)):额外代码注入到程序源代码中。

示例:

// 原始程序
void sci() {
    int num = 0;
    for (int i=0; i<100; ++i) {
        num += 1;
        if (i == 50) {
            break;
        }
    }
    printf("%d", num);
}
// 插桩后的程序
char inst[5];
void sci() {
    int num = 0;
    inst[0] = 1;
    for (int i=0; i<100; ++i) {
        num += 1;
        inst[1] = 1;
        if (i == 50) {
            inst[2] = 1;
            break;
        }
        inst[3] = 1;
    }
    printf("%d", num);
    inst[4] = 1;
}

二进制插桩(Binary Instrumentation(BI)):额外代码注入到二进制可执行文件中。

  • 静态二进制插桩:在程序执行前插入额外的代码和数据,生成一个永久改变的可执行文件。
  • 动态二进制插桩:在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久改变。

以上面的函数 sci 生成的汇编为例:

原始汇编代码

sci:
  pushl %ebp
  movl  %esp, %ebp
  pushl %ebx
  subl  $20, %esp
  call  __x86.get_pc_thunk.ax
  addl  $_GLOBAL_OFFSET_TABLE_, %eax
  movl  $0, -16(%ebp)
  movl  $0, -12(%ebp)
  jmp   .L2
  • 插入指令计数代码

    sci:
      counter++;
      pushl %ebp
      counter++;
      movl  %esp, %ebp
      counter++;
      pushl %ebx
      counter++;
      subl  $20, %esp
      counter++;
      call  __x86.get_pc_thunk.ax
      counter++;
      addl  $_GLOBAL_OFFSET_TABLE_, %eax
      counter++;
      movl  $0, -16(%ebp)
      counter++;
      movl  $0, -12(%ebp)
      counter++;
      jmp   .L2
    
  • 插入指令跟踪代码

sci:
  Print(ip)
  pushl %ebp
  Print(ip)
  movl  %esp, %ebp
  Print(ip)
  pushl %ebx
  Print(ip)
  subl  $20, %esp
  Print(ip)
  call  __x86.get_pc_thunk.ax
  Print(ip)
  addl  $_GLOBAL_OFFSET_TABLE_, %eax
  Print(ip)
  movl  $0, -16(%ebp)
  Print(ip)
  movl  $0, -12(%ebp)
  Print(ip)
  jmp   .L

Pin 简介

Pin 是 Intel 公司研发的一个动态二进制插桩框架,可以在二进制程序运行过程中插入各种函数,以监控程序每一步的执行。官网(目前有 2.x 和 3.x 两个版本,2.x 不能在 Linux 内核 4.x 及以上版本上运行,这里我们选择 3.x)

Pin 具有以下优点:

  • 易用
    • 使用动态插桩,不需要源代码、不需要重新编译和链接。
  • 可扩展
    • 提供了丰富的 API,可以使用 C/C++ 编写插桩工具(被叫做 Pintools)
  • 多平台
    • 支持 x86、x86-64、Itanium、Xscale
    • Windows、Linux、OSX、Android
  • 鲁棒性
    • 支持插桩现实世界中的应用:数据库、浏览器等
    • 支持插桩多线程应用
    • 支持信号量
  • 高效
    • 在指令代码层面实现编译优化

Pin 的基本结构和原理

Pin 是一个闭源的框架,由 Pin 和 Pintool 组成。Pin 内部提供 API,用户使用 API 编写可以由 Pin 调用的动态链接库形式的插件,称为 Pintool。

由图可以看出,Pin 由进程级的虚拟机、代码缓存和提供给用户的插桩检测 API 组成。Pin 虚拟机包括 JIT(Just-In-Time) 编译器、模拟执行单元和代码调度三部分,其中核心部分为 JIT 编译器。当 Pin 将待插桩程序加载并获得控制权之后,在调度器的协调下,JIT 编译器负责对二进制文件中的指令进行插桩,动态编译后的代码即包含用户定义的插桩代码。编译后的代码保存在代码缓存中,经调度后交付运行。

程序运行时,Pin 会拦截可执行代码的第一条指令,并为后续指令序列生成新的代码,新代码的生成即按照用户定义的插桩规则在原始指令的前后加入用户代码,通过这些代码可以抛出运行时的各种信息。然后将控制权交给新生成的指令序列,并在虚拟机中运行。当程序进入到新的分支时,Pin 重新获得控制权并为新分支的指令序列生成新的代码。

通常插桩需要的两个组件都在 Pintool 中:

  • 插桩代码(Instrumentation code)
    • 在什么位置插入插桩代码
  • 分析代码(Analysis code)
    • 在选定的位置要执行的代码

Pintool 采用向 Pin 注册插桩回调函数的方式,对每一个被插桩的代码段,Pin 调用相应的插桩回调函数,观察需要产生的代码,检查它的静态属性,并决定是否需要以及插入分析函数的位置。分析函数会得到插桩函数传入的寄存器状态、内存读写地址、指令对象、指令类型等参数。

  • Instrumentation routines:仅当事件第一次发生时被调用
  • Analysis routines:某对象每次被访问时都调用
  • Callbacks:无论何时当特定事件发生时都调用

Pin 的基本用法

在 Pin 解压后的目录下,编译一个 Pintool,首先在 source/tools/ 目录中创建文件夹 MyPintools,将 mypintool.cpp 复制到 source/tools/MyPintools 目录下,然后 make

$ cp mypintools.cpp source/tools/MyPintools
$ cd source/tools/MyPintools

对于 32 位架构,使用 TARGET=ia32

[MyPintools]$ make obj-ia32/mypintool.so TARGET=ia32

对于 64 位架构,使用 TARGET=intel64

[MyPintools]$ make obj-intel64/mypintool.so TARGET=intel64

启动并插桩一个应用程序:

[MyPintools]$ ../../../pin -t obj-intel64/mypintools.so -- application

其中 pin 是插桩引擎,由 Pin 的开发者提供;pintool.so 是插桩工具,由用户自己编写并编译。

绑定并插桩一个正在运行的程序:

[MyPintools]$ ../../../pin -t obj-intel64/mypintools.so -pid 1234

Pintool 示例分析

Pin 提供了一些 Pintool 的示例,下面我们分析一下用户手册中介绍的指令计数工具,可以在 source/tools/ManualExamples/inscount0.cpp 中找到。

#include <iostream>
#include <fstream>
#include "pin.H"
ofstream OutFile;
// The running count of instructions is kept here
// make it static to help the compiler optimize docount
static UINT64 icount = 0;
// This function is called before every instruction is executed
VOID docount() { icount++; }
// Pin calls this function every time a new instruction is encountered
VOID Instruction(INS ins, VOID *v)
{
    // Insert a call to docount before every instruction, no arguments are passed
    INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END);
}
KNOB<string> KnobOutputFile(KNOB_MODE_WRITEONCE, "pintool",
    "o", "inscount.out", "specify output file name");
// This function is called when the application exits
VOID Fini(INT32 code, VOID *v)
{
    // Write to a file since cout and cerr maybe closed by the application
    OutFile.setf(ios::showbase);
    OutFile << "Count " << icount << endl;
    OutFile.close();
}
/* ===================================================================== */
/* Print Help Message                                                    */
/* ===================================================================== */
INT32 Usage()
{
    cerr << "This tool counts the number of dynamic instructions executed" << endl;
    cerr << endl << KNOB_BASE::StringKnobSummary() << endl;
    return -1;
}
/* ===================================================================== */
/* Main                                                                  */
/* ===================================================================== */
/*   argc, argv are the entire command line: pin -t <toolname> -- ...    */
/* ===================================================================== */
int main(int argc, char * argv[])
{
    // Initialize pin
    if (PIN_Init(argc, argv)) return Usage();
    OutFile.open(KnobOutputFile.Value().c_str());
    // Register Instruction to be called to instrument instructions
    INS_AddInstrumentFunction(Instruction, 0);
    // Register Fini to be called when the application exits
    PIN_AddFiniFunction(Fini, 0);
    // Start the program, never returns
    PIN_StartProgram();
    return 0;
}

执行流程如下:

  • 在主函数

    main
    

    中:

    • 初始化 PIN_Init(),注册指令粒度的回调函数 INS_AddInstrumentFunction(Instruction, 0),被注册插桩函数名为 Instruction
    • 注册完成函数(常用于最后输出结果)
    • 启动 Pin 执行
  • 在每条指令之前(IPOINT_BEFORE)执行分析函数 docount(),功能是对全局变量递增计数。

  • 执行完成函数 Fini(),输出计数结果到文件。

由于我当前使用的系统和内核版本过新,Pin 暂时还未支持,使用时需要加上 -ifeellucky 参数(在最新的 pin 3.5 中似乎不需要这个参数了),-o 参数将运行结果输出到文件。运行程序:

[ManualExamples]$ uname -a
Linux manjaro 4.11.5-1-ARCH #1 SMP PREEMPT Wed Jun 14 16:19:27 CEST 2017 x86_64 GNU/Linux
[ManualExamples]$ ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount0.log -- /bin/ls
[ManualExamples]$ cat inscount0.log
Count 528090

其它一些自带插件及功能如下:

插件名功能
inscount统计执行的指令数量,输出到 inscount.out 文件
itrace记录执行指令的 eip
malloctrace记录 malloc 和 free 的调用情况
pinatrace记录读写内存的位置和值
proccount统计 Procedure 的信息,包括名称、镜像、地址、指令数
w_malloctrace记录 RtlAllocateHeap 的调用情况

Pintool 编写

main 函数的编写

Pintool 的入口为 main 函数,通常需要完成下面的功能:

  • 初始化 Pin 系统环境:

    • BOOL LEVEL_PINCLIENT::PIN_Init(INT32 argc, CHAR** argv)
  • 初始化符号表(如果需要调用程序符号信息,通常是指令粒度以上):

    • VOID LEVEL_PINCLIENT::PIN_InitSymbols()
  • 初始化同步变量:

    • Pin 提供了自己的锁和线程管理 API 给 Pintool 使用。当 Pintool 对多线程程序进行二进制检测,需要用到全局变量时,需要利用 Pin 提供的锁(Lock)机制,使得全局变量的访问互斥。编写时在全局变量中声明锁变量并在 main 函数中对锁进行初始化:VOID LEVEL_BASE::InitLock(PIN_LOCK *lock)。在插桩函数和分析函数中,锁的使用方式如下,应注意在全局变量使用完毕后释放锁,避免死锁的发生:
    GetLock(&thread_lock, threadid);
    // 访问全局变量
    ReleaseLock(&thread_lock);
    
  • 注册不同粒度的回调函数:

    • TRACE(轨迹)粒度

      • TRACE 表示一个单入口、多出口的指令序列的数据结构。Pin 将 TRACE 分为若干基本块 BBL(Basic Block),一个 BLL 是一个单入口、单出口的指令序列。TRACE 在指令发生跳转时进行插入,进一步进行基本块分析,常用于记录程序执行序列。注册 TRACE 粒度插桩函数原型为:

        TRACE_AddInstrumentFunction(TRACE_INSTRUMENT_CALLBACK fun, VOID *val)
        
    • IMG(镜像)粒度

      • IMG 表示整个被加载进内存的二进制可执行模块(如可执行文件、动态链接库等)类型的数据结构。每次被插桩进程在执行过程中加载了镜像类型文件时,就会被当做 IMG 类型处理。注册插桩 IMG 粒度加载和卸载的函数原型:

        IMG_AddInstrumentFunction(IMAGECALLBACK fun, VOID *v)
        IMG_AddUnloadFunction(IMAGECALLBACK fun, VOID *v)
        
    • RTN(例程)粒度

      • RTN 代表了由面向过程程序语言编译器产生的函数/例成/过程。Pin 使用符号表来查找例程,即需要插入的位置,需要调用内置的初始化表函数

        PIN_InitSymbols()
        

        。必须使用

        PIN_InitSymbols
        

        使得符号表信息可用。插桩 RTN 粒度函数原型:

        RTN_AddInstrumentFunction(RTN_INSTRUMENT_CALLBACK fun, VOID *val)
        
    • INS(指令)粒度

      • INS 代表一条指令对应的数据结构,INS 是最小的粒度。INS 的代码插桩是在指令执行前、后插入附加代码,会导致程序执行缓慢。插桩 INS 粒度函数原型:

        INS_AddInstrumentFunction(INS_INSTRUMENT_CALLBACK fun, VOID *val)
        
  • 注册结束回调函数

    • 插桩程序运行结束时,可以调用结束函数来释放不再使用的资源,输出统计结果等。注册结束回调函数:
    VOID PIN_AddFiniFunction(FINI_CALLBACK fun, VOID *val)
    
  • 启动 Pin 虚拟机进行插桩:

    • 最后调用 VOID PIN_StartProgram() 启动程序的运行。

插桩、分析函数的编写

main 函数中注册插桩回调函数后,Pin 虚拟机将在运行过程中对该种粒度的插桩函数对象选择性的进行插桩。所谓选择性,就是根据被插桩对象的性质和条件,选择性的提取或修改程序执行过程中的信息。

各种粒度的插桩函数:

  • INS
    • VOID LEVEL_PINCLIENT::INS_InsertCall(INS ins, IPOINT action, AFUNPTR funptr, ...)
  • RTN
    • VOID LEVEL_PINCLIENT::RTN_InsertCall(RTN rtn, IPOINT action, AFUNPTR funptr, ...)
  • TRACE
    • VOID LEVEL_PINCLIENT::TRACE_InsertCall(TRACE trace, IPOINT action, AFUNPTR funptr, ...)
  • BBL
    • VOID LEVEL_PINCLIENT::BBL_InsertCall(BBL bbl, IPOINT action, AFUNPTR funptr, ...)

其中 funptr 为用户自定义的分析函数,函数参数与 ... 参数列表传入的参数个数相同,参数列表以 IARG_END 标记结束。

Pin 在 CTF 中的应用

由于程序具有循环、分支等结构,每次运行时执行的指令数量不一定相同,于是我们可是使用 Pin 来统计执行指令的数量,从而对程序进行分析。特别是对一些使用特殊指令集和虚拟机,或者运用了反调试等技术的程序来说,相对于静态分析去死磕,动态插桩技术是一个比较好的选择。

我们先举一个例子,源码如下:

#include<stdio.h>
#include<string.h>
void main() {
    char pwd[] = "abc123";
    char str[128];
    int flag = 1;
    scanf("%s", str);
    for (int i=0; i<=strlen(pwd); i++) {
        if (pwd[i]!=str[i] || str[i]=='\0'&&pwd[i]!='\0' || str[i]!='\0'&&pwd[i]=='\0') {
            flag = 0;
        }
    }
    if (flag==0) {
        printf("Bad!\n");
    } else {
        printf("Good!\n");
    }
}

这段代码要求用户输入密码,然后逐字符进行判断。

使用前面分析的指令计数的 inscount0 Pintool,我们先测试下密码的长度:

[ManualExamples]$ echo x | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152667
[ManualExamples]$ echo xx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152688
[ManualExamples]$ echo xxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152709
[ManualExamples]$ echo xxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152730
[ManualExamples]$ echo xxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152751
[ManualExamples]$ echo xxxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152772
[ManualExamples]$ echo xxxxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152779

我们输入的密码位数从 1 到 7,可以看到输入位数为 6 位或更少时,计数值之差都是 21,而输入 7 位密码时,差值仅为 7,不等于 21。于是我们知道程序密码为 6 位。接下来我们更改密码的第一位:

[ManualExamples]$ echo axxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152786
[ManualExamples]$ echo bxxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152772
[ManualExamples]$ echo cxxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152772
[ManualExamples]$ echo dxxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152772

很明显,程序密码第一位是 a,接着尝试第二位:

[ManualExamples]$ echo aaxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152786
[ManualExamples]$ echo abxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152800
[ManualExamples]$ echo acxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152786
[ManualExamples]$ echo adxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
Bad!
Count 152786

第二位是 b,同时我们还可以发现,每一位正确与错误的指令计数之差均为 14。同理,我们就可以暴力破解出密码,但这种暴力破解方式大大减少了次数,提高了效率。破解脚本可查看参考资料。

参考资料

扩展:Triton

Triton 是一个二进制执行框架,其具有两个重要的优点,一是可以使用 Python 调用 Pin,二是支持符号执行。官网

5.6 LLVM

LLVM项目是可重用(reusable)、模块化(modular)的编译器以及工具链技术(toolchain technologies)的集合。有人将其理解为“底层虚拟机(Low Level Virtual Machine)”的简称,但是官方原话为:

“The name “LLVM” itself is not an acronym; it is the full name of the project.”

意思是:LLVM不是首字母缩写,而是这整个项目的全名。

LLVM项目的发展起源于2000年伊利诺伊大学厄巴纳-香槟分校维克拉姆·艾夫(Vikram Adve)与克里斯·拉特纳(Chris Lattner)的研究,他们想要为所有静态及动态语言创造出动态的编译技术。2005年,苹果计算机雇用了克里斯·拉特纳及他的团队为苹果计算机开发应用程序系统,LLVM为现今Mac OS X及iOS开发工具的一部分。

2.LLVM简介

用户文档:llvm.org/docs/LangRef.html

LLVM是基于静态单一分配的表示形式,可提供类型安全性、底层操作、灵活性,并且适配几乎所有高级语言,具有通用的代码表示。现在LLVM已经成为多个编译器和代码生成相关子项目的母项目。

The LLVM code representation is designed to be used in three different forms: as an in-memory compiler IR, as an on-disk bitcode representation (suitable for fast loading by a Just-In-Time compiler), and as a human readable assembly language representation.

其中,LLVM提供了完整编译系统的中间层,并将中间语言(Intermediate Repressentation, IR)从编译器取出并进行最优化,最优化后的IR接着被转换及链接到目标平台的汇编语言。

我们知道,传统的编译器主要结构为:

(https://clheveningflow.github.io/2019/09/28/LLVM1/4.jpg)

传统编译器结构,图片摘自网络

Frontend:前端,词法分析、语法分析、语义分析、生成中间代码

Optimizer:优化器,进行中间代码优化

Backend:后端,生成机器码

LLVM主要结构为:
在这里插入图片描述

也就是说,对于LLVM来说,不同的前后端使用统一的中间代码LLVM IR。如果需要支持一种新的编程语言/硬件设备,那么只需要实现一个新的前端/后端就可以了,而优化截断是一个通用的阶段,针对统一的LLVM IR,都不需要对于优化阶段修改。对比GCC,其前端和后端基本耦合在一起,所以GCC支持一门新的语言或者目标平台会变得很困难。

以下内容摘自维基百科:

LLVM也可以在编译时期、链接时期,甚至是运行时期产生可重新定位的代码(Relocatable Code)。

LLVM支持与语言无关的指令集架构及类型系统。每个在静态单赋值形式(SSA)的指令集代表着,每个变量(被称为具有类型的寄存器)仅被赋值一次,这简化了变量间相依性的分析。LLVM允许代码被静态的编译,包含在传统的GCC系统底下,或是类似JAVA等后期编译才将IF编译成机器代码所使用的即时编译(JIT)技术。它的类型系统包含基本类型(整数或是浮点数)及五个复合类型(指针、数组、向量、结构及函数),在LLVM具体语言的类型建制可以以结合基本类型来表示,举例来说,C++所使用的class可以被表示为结构、函数及函数指针的数组所组成。

LLVM JIT编译器可以最优化在运行时期时程序所不需要的静态分支,这在一些部分求值(Partial Evaluation)的案例中相当有效,即当程序有许多选项,而在特定环境下其中多数可被判断为是不需要。这个特色被使用在Mac OS X Leopard(v10.5)底下OpenGL的管线,当硬件不支持某个功能时依然可以被成功地运作。OpenGL堆栈下的绘图程序被编译为IR,接着在机器上运行时被编译,当系统拥有高端GPU时,这段程序会进行极少的修改并将传递指令给GPU,当系统拥有低级的GPU时,LLVM将会编译更多的程序,使这段GPU无法运行的指令在本地端的中央处理器运行。LLVM增进了使用Intel GMA芯片等低端机器的性能。一个类似的系统发展于Gallium3D LLVMpipe,它已被合并到GNOME,使其可运行在没有GPU的环境。

根据2011年的一项测试,GCC在运行时期的性能平均比LLVM高10%。而2013年测试显示,LLVM可以编译出接近GCC相同性能的运行码。

简介

LLVM 是当今炙手可热的编译器基础框架。它从一开始就采用了模块化设计的思想,使得每一个编译阶段都被独立出来,形成了一系列的库。LLVM 使用面向对象的 C++ 语言开发,为编译器开发人员提供了易用而丰富的编程接口和 API。

学过编译原理的人都知道,编译过程主要可以划分为前端与后端:

  • 前端把源代码翻译成中间表示 (IR)。
  • 后端把IR编译成目标平台的机器码。当然,IR也可以给解释器解释执行。

然而,经典的编译器如gcc在设计上都是提供一条龙服务的: 你不需要知道它使用的IR是什么样的,它也不会暴露中间接口来给你操作它的IR。 换句话说,从前端到后端,这些编译器的大量代码都是强耦合的。

这样做有好处也有坏处。好处是,因为不需要暴露中间过程的接口,它可以在内部做任何想做的平台相关的优化。 而坏处是,每当一个新的平台出现,这些编译器都要各自为政实现一个从自己的IR到新平台的后端。 甚至如果当一种新语言出现,且需要实现一个新的编译器,那么可能需要设计一个新的IR,以及针对大部分平台实现这个IR的后端。 不妨想一下,如果有M种语言、N种目标平台,那么最坏情况下要实现 M*N 个前后端。这是很低效的。

因此,我们很自然地会想,如果大家都共用一种IR呢? 那么每当新增加一种语言,我们就只要添加一个这个语言到IR的前端; 每当新增加一种目标平台,我们就只要添加一个IR到这个目标平台的后端。 如果有M种语言、N种目标平台,那么最优情况下我们只要实现 M+N 个前后端。

LLVM就是这样一个项目。LLVM的核心设计了一个叫 LLVM IR 的中间表示, 并以库(Library) 的方式提供一系列接口, 为你提供诸如操作IR、生成目标平台代码等等后端的功能。

那么 LLVM Pass 又是什么呢? Pass就是“遍历一遍IR,可以同时对它做一些操作”的意思。翻译成中文应该叫“趟”。 在实现上,LLVM的核心库中会给你一些 Pass类 去继承。你需要实现它的一些方法。 最后使用LLVM的编译器会把它翻译得到的IR传入Pass里,给你遍历和修改。

那LLVM Pass有什么用呢?

  1. 显然它的一个用处就是插桩,毕竟这是我本来想利用它做的事情: 在Pass遍历LLVM IR的同时,自然就可以往里面插入新的代码。
  2. 机器无关的代码优化:大家如果还记得编译原理的知识的话,应该知道IR在被翻译成机器码前会做一些机器无关的优化。 但是不同的优化方法之间需要解耦,所以自然要各自遍历一遍IR,实现成了一个个LLVM Pass。 最终,基于LLVM的编译器会在前端生成LLVM IR后调用一些LLVM Pass做机器无关优化, 然后再调用LLVM后端生成目标平台代码。
  3. 静态分析: 像VSCode的C/C++插件就会用LLVM Pass来分析代码,提示可能的错误 (无用的变量、无法到达的代码等等)。
  4. …… (自行发挥想象)

再次强调,LLVM的核心是一个库,而不是一个具体的二进制程序。 不过,LLVM这个项目本身也基于这个库实现了周边的工具, 下面列出了几个重要的命令行工具,光看名字就可以知道它们大概在做什么:

  • llvm-as:把LLVM IR从人类能看懂的文本格式汇编成二进制格式。注意:此处得到的不是目标平台的机器码。
  • llvm-disllvm-as的逆过程,即反汇编。 不过这里的反汇编的对象是LLVM IR的二进制格式,而不是机器码。
  • opt:优化LLVM IR。输出新的LLVM IR。
  • llc:把LLVM IR编译成汇编码。需要用as进一步得到机器码。
  • lli:解释执行LLVM IR。 9

如果现在无法想象什么“文本格式”“二进制格式”也没关系。 在后面的LLVM IR一节中,读者了解完LLVM IR后,我会再简单介绍一下这些指令的使用。

另外,此时在有了LLVM的概念以后,我们可以打开github上的LLVM的源代码

LLVM IR

所有 LLVM 指令都使用 SSA (Static Single Assignment) 方式表示。所有变量都只能被赋值一次**,这样做主要是便于后期的代码优化。

大家肯定知道,LLVM IR实际上有三种表示:

  1. .ll 格式:人类可以阅读的文本。
  2. .bc 格式:适合机器存储的二进制文件。
  3. 内存表示

首先,.ll格式和.bc格式是如何生成并相互转换的呢?下面我列了个常用的简单指令清单:

  • .c -> .ll:clang -emit-llvm -S a.c -o a.ll

  • .c -> .bc: clang -emit-llvm -c a.c -o a.bc

  • .ll -> .bc: llvm-as a.ll -o a.bc

  • .bc -> .ll: llvm-dis a.bc -o a.ll

  • .bc -> .s: llc a.bc -o a.s (汇编)

初步使用

首先我们通过著名的 helloWorld 来熟悉下 LLVM 的 2 2 2 2 2 2 2 2 2使用。

#include <stdio.h>
int main()
{
    printf("hello, world\n");
}

将 C 源码转换成 LLVM 汇编码:

$ clang -emit-llvm -S hello.c -o hello.ll

生成的 LLVM IR 如下:

; ModuleID = 'hello.c'
source_filename = "hello.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
@.str = private unnamed_addr constant [14 x i8] c"hello, world\0A\00", align 1
; Function Attrs: noinline nounwind optnone sspstrong uwtable
define i32 @main() #0 {
  %1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}
declare i32 @printf(i8*, ...) #1
attributes #0 = { noinline nounwind optnone sspstrong uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2}
!llvm.ident = !{!3}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{!"clang version 5.0.1 (tags/RELEASE_501/final)"}

该过程从词法分析开始,将 C 源码分解成 token 流,然后传递给语法分析器,语法分析器在 CFG(上下文无关文法)的指导下将 token 流组织成 AST(抽象语法树),接下来进行语义分析,检查语义正确性,最后生成 IR。

LLVM bitcode 有两部分组成:位流,以及将 LLVM IR 编码成位流的编码格式。使用汇编器 llvm-as 将 LLVM IR 转换成 bitcode:

$ llvm-as hello.ll -o hello.bc

结果如下:

$ file hello.bc
hello.bc: LLVM IR bitcode
$ xxd -g1 hello.bc | head -n5
00000000: 42 43 c0 de 35 14 00 00 05 00 00 00 62 0c 30 24  BC..5.......b.0$
00000010: 49 59 be 66 ee d3 7e 2d 44 01 32 05 00 00 00 00  IY.f..~-D.2.....
00000020: 21 0c 00 00 4d 02 00 00 0b 02 21 00 02 00 00 00  !...M.....!.....
00000030: 13 00 00 00 07 81 23 91 41 c8 04 49 06 10 32 39  ......#.A..I..29
00000040: 92 01 84 0c 25 05 08 19 1e 04 8b 62 80 10 45 02  ....%......b..E.

反过来将 bitcode 转回 LLVM IR 也是可以的,使用反汇编器 llvm-dis:

$ llvm-dis hello.bc -o hello.ll

其实 LLVM 可以利用工具 lli 的即时编译器(JIT)直接执行 bitcode 格式的程序:

$ lli hello.bchello, world

接下来使用静态编译器 llc 命令可以将 bitcode 编译为特定架构的汇编语言:

$ llc -march=x86-64 hello.bc -o hello.s

也可以使用 clang 来生成,结果是一样的:

$ clang -S hello.bc -o hello.s -fomit-frame-pointer

结果如下:

        .text
        .file   "hello.c"
        .globl  main                    # -- Begin function main
        .p2align        4, 0x90
        .type   main,@function
main:                                   # @main
        .cfi_startproc
# BB#0:
        pushq   %rbp
.Lcfi0:
        .cfi_def_cfa_offset 16
.Lcfi1:
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
.Lcfi2:
        .cfi_def_cfa_register %rbp
        movabsq $.L.str, %rdi
        movb    $0, %al
        callq   printf
        xorl    %eax, %eax
        popq    %rbp
        retq
.Lfunc_end0:
        .size   main, .Lfunc_end0-main
        .cfi_endproc
                                        # -- End function
        .type   .L.str,@object          # @.str
        .section        .rodata.str1.1,"aMS",@progbits,1
.L.str:
        .asciz  "hello, world\n"
        .size   .L.str, 14
        .ident  "clang version 5.0.1 (tags/RELEASE_501/final)"
        .section        ".note.GNU-stack","",@progbits

5.6.1 Clang

简介

Clang 一个基于 LLVM 的编译器前端,支持 C/C++/Objective-C 等语言。其开发目标是替代 GCC。

在软件安全的应用中,已经有许多代码分析工具都基于 Clang 和 LLVM,开发社区也都十分活跃。

初步使用

首先我们来编译安装 LLVM 和 Clang:

$ svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
$ cd llvm/tools
$ svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
$ svn co http://llvm.org/svn/llvm-project/lld/trunk lld # optional
$ svn co http://llvm.org/svn/llvm-project/polly/trunk polly # optional
$ cd clang/tools
$ svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra # optional
$ cd ../../../.. && cd llvm/projects
$ svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt # optional
$ svn co http://llvm.org/svn/llvm-project/openmp/trunk openmp # optional
$ svn co http://llvm.org/svn/llvm-project/libcxx/trunk libcxx # optional
$ svn co http://llvm.org/svn/llvm-project/libcxxabi/trunk libcxxabi # optional
$ svn co http://llvm.org/svn/llvm-project/test-suite/trunk test-suite # optional
$ cd ../.. && cd llvm
$
$ mkdir build && cd build
$ cmake -G Ninja ../
$ cmake --build .
$ cmake --build . --target install

内部实现

Clang 前端的主要流程如下:

Driver -> Lex -> Parse -> Sema -> CodeGen (LLVM IR)

CMake

CMake是个一个开源的跨平台自动化建构系统,用来管理软件建置的程序,并不相依于某特定编译器。

并可支持多层目录、多个应用程序与多个库。 它用配置文件控制建构过程(build process)的方式和Unix的make相似,只是CMake的配置文件取名为CMakeLists.txt。

CMake并不直接建构出最终的软件,而是产生标准的建构档(如Unix的Makefile或Windows Visual C++的projects/workspaces),然后再依一般的建构方式使用。

这使得熟悉某个集成开发环境(IDE)的开发者可以用标准的方式建构他的软件,这种可以使用各平台的原生建构系统的能力是CMake和SCons等其他类似系统的区别之处。

它首先允许开发者编写一种平台无关的CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。

“CMake”这个名字是"Cross platform MAke"的缩写。虽然名字中含有"make",但是CMake和Unix上常见的“make”系统是分开的,而且更为高端。 它可与原生建置环境结合使用,例如:make、苹果的Xcode与微软的Visual Studio。

插桩

1. 编译时程序插桩(Compile-time Instrument)

程序插桩是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针代码,以在程序动态执行时收集内部运行信息,用于进行程序分析或安全分析。典型地,可以通过插桩获取程序的控制流(例如代码覆盖情况)和数据流(例如运行时变量值)信息。代表性的基于程序插桩的工具/框架有LLVM DataSanitizer、American Fuzzy Lop、SymCC等,对程序分析、软件安全测试具有重要意义。

程序插桩主要有三类方案,适用于不同的目标和场景:

  • 编译时插桩:适用于有源码的白盒情况,通常在中间语言上进行插桩,插桩的代码能够从编译优化中收益,是开销最低的插桩方案。
  • 动态二进制插桩:适用于无源码的黑盒情况。大多数动态二进制插桩框架都有三种执行模式:解释模式( Interpretation mode)、探测模式(probe mode)和JIT模式(just-in-time mode)。JIT模式是最常用的实现方式,例如Intel Pin。但同时,相比编译时插桩,无论解释还是JIT都引入了更多的额外开销。
  • 静态二进制插桩:适用于无源码的黑盒情况。静态二进制插桩主要是通过二进制重写技术来实现的,即在汇编语言的级别上进行插桩,插桩的代码是直接写入目标程序的二进制文件中的。静态二进制插桩的效率通常认为高于动态二进制插桩,但低于编译插桩。目前Linux平台上二进制重写技术比较成熟,但windows平台上的静态二进制插桩框架还比较少(WinAFL就应用了静态二进制插桩技术来提升效率)。

程序插桩技术广泛应用于程序分析/自动化漏洞挖掘研究与实践中,例如:

  • 基于覆盖率反馈的模糊测试(AFL)
  • 污点分析(LLVM Dsan, Angora)
  • 符号执行(SymCC)

LLVM框架在编译时允许用户编写自己的LLVM Pass,在中间语言(LLVM IR)级别进行程序插桩,结合LLVM IR提供的丰富程序信息,可以结合静态分析更好地进行插桩。

个人认为学习LLVM编译插桩最好的途径是找到应用该技术的一些开源项目,例如AFL,结合官方文档进行学习。以下,仅分享一些我们之前工作中用到的有意思的API的示例代码

2. 插桩分支代码:BranchInst

2.1 splitblockandinsertifthenelse()

stackoverflow

2.2 SplitBlockAndInsertIfThen()

或者仅仅想插桩if ... then ...的逻辑,就可以用SplitBlockAndInsertIfThen(),其使用相对简单些,一个例子如下:

Value``*` `val_c ``=` `NULL;``IRBuilder<> IRB(InsertPoint);``Value``*` `cmp` `=` `IRB.CreateICmpEQ(val_a, val_b);``BranchInst ``*``BI ``=` `cast<BranchInst>(``   ``SplitBlockAndInsertIfThen(``cmp``, InsertPoint, false, CallWeights));``/``*` `Instrument at new basic block ``*``/``IRBuilder<> ThenB(BI);``val_c ``=` `ThenB.CreateAdd(val_a, val_b);``val_c ``=` `IRB.CreateSub(val_a, val_b);

上述插桩将在目标程序中插入如下代码:

if` `(val_a ``=``=` `val_b) {``   ``val_c ``=` `val_a ``+` `val_b;``}``val_c ``=` `val_a ``-` `val_b;

值得注意的是,splitblockandinsertifthenelse()的第三个参数可以由用户可选地提供一个branch weight的参数,指定该分支的通过概率以便于进行优化(类似unlikely?)。

3. getIntNTy()

LLVM IR中惯用的IntegerType主要是:

  • Int8Ty
  • Int16Ty
  • Int32Ty
  • Int64Ty

但是,今天注意到了一个有意思的API:

static IntegerType ``*`   `getIntNTy (LLVMContext &C, unsigned N)

从而,我们可以定义任意长度的IntegerType,能够更加灵活地使用LLVM玩出花样。以下给出一个例子,在这个例子中,我们希望提取字符串的前N字节(N <= 8)。

3.1 一个小练习

原始程序demo.c:

unsigned long long str2hex(char *buf) {
      return 0;
}
int main() {
      char buf[32];
      scanf("%s", buf);
      printf("Hex: %8llx\n", str2hex(buf));
      return 0;
}

我们的目标是插桩democ.c的str2hex()函数,使该函数返回char *buf的前N = 6个字节的Hex形式。

为了实现该目标,我们需要编写一个LLVM Pass,其中主要逻辑如下:

for (llvm::Function &F : M) {
      if (F.getName() != "str2hex") continue;
      IntegerType *IntNTy = IntegerType::getIntNTy(C, 48); // 6 * 8 = 48 bits
      IntegerType *Int64Ty = IntegerType::getInt64Ty(C);
      BasicBlock::iterator IP = F.getEntryBlock().getFirstInsertionPt();
      Value* arg = &(*F.arg_begin());
      IRBuilder<> IRB(&(*IP));
      Value *ptr = IRB.CreateBitCast(IRB.CreateGEP(arg, ConstantInt::get(Int64Ty, 0)), IntNTy->getPointerTo());
      Value *hex = IRB.CreateLoad(ptr);
      hex = IRB.CreateZExt(hex, Int64Ty);
      for (auto &I : *IP->getParent()) {
            if (dyn_cast<llvm::ReturnInst>(&I)) {
                  llvm::ReturnInst *new_inst = llvm::ReturnInst::Create(m->getContext(), hex);
                  ReplaceInstWithInst(&I, new_inst);
                  break;
            }c
      }
}

插桩后的demo.ll如下:

define i64 @str2hex(i8*) #3 {
  %2 = getelementptr i8, i8* %0, i64 0
  %3 = bitcast i8* %2 to i48*
  %4 = load i48, i48* %3
  %5 = zext i48 %4 to i64
  %6 = alloca i8*, align 8
  store i8* %0, i8** %6, align 8
  ret i64 %5
}

3.2 IntNTy在x86上的实现方式

之所以LLVM IR中的Int8TyInt16TyInt32Ty更常用,是因为它们对应了ByteWordDword等类型,可以直接通过汇编指令表示。为了探究IntNTy,即任意长度的整型在x86汇编中的实现,我们将上一小节中的demo.c编译成可执行文件,并通过gdb反汇编来查看插桩代码的实现:

push   rbp
mov    rbp,rsp
mov    eax,DWORD PTR [rdi]
mov    ecx,eax
movzx  eax,WORD PTR [rdi+0x4]
mov    edx,eax
shl    rdx,0x20
or     rcx,rdx
mov    QWORD PTR [rbp-0x8],rdi
mov    rax,rcx
pop    rbp
ret

可以看到,对于6字节的IntNTy,汇编中首先读取一个4字节的Dword,接着读取一个2字节的Word,然后通过“移位”与“或”操作组合成一个6字节的值。

显然,IntNty提供了更方便的方式,让我们可以在插桩时使用一个任意长度的整型值。但是,从汇编角度,当LLVM IR最终编译成汇编代码时,IntNTy仍然是基于Byte、Dword等类型实现,并且仍然需要引入额外的开销来进行组合。LLVM IR只不过是将基础类型“组合”的部分进行了封装,提供了便捷但并不会提升效率。

4. Predecessors与Successors

类似IDAPython,在基本块的级别获取其前继/后继。例如:

for (llvm::BasicBlock* pred_bb : predecessors(cur_bb)) {
   printf("prev_bb=%p\n", pred_bb); /* for debug */
}

4.1 问题:predecessors()可能返回重复的前驱基本块

这里需要注意,我们遇到过一个奇怪的情况(通常发生在switch结构中),即predecessors()返回多个相同的基本块指针:

prev_bb=0x56407eaee190
prev_bb=0x56407eaee190
prev_bb=0x56407eaee190
prev_bb=0x56407eaee190
prev_bb=0x56407eaee190

Github Issue中有人遇到了相同的情况:#76. 目前,我们只能在调用predecessors()后手动进行检查和去重。

4.2 unique predecessor/successors

查看LLVM BasicBlock类的文档时,发现了一些有意思的成员函数

BasicBlock *getSingleSuccessor () const
     Return the successor of this block if it has a single successor. More...
 
BasicBlock *getUniqueSuccessor () const
     Return the successor of this block if it has a unique successor. More...

我们主要疑惑于其中"Unique"的含义。在查阅源码注释后,基本得到了解答:

/// Note that unique predecessor doesn't mean single edge, there can be
/// multiple edges from the unique predecessor to this block (for example a
/// switch statement with multiple cases having the same destination).

最后,本篇只是做一个简单的分享,欢迎批评与指正。

具。**

“CMake”这个名字是"Cross platform MAke"的缩写。虽然名字中含有"make",但是CMake和Unix上常见的“make”系统是分开的,而且更为高端。 它可与原生建置环境结合使用,例如:make、苹果的Xcode与微软的Visual Studio。

插桩

1. 编译时程序插桩(Compile-time Instrument)

程序插桩是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针代码,以在程序动态执行时收集内部运行信息,用于进行程序分析或安全分析。典型地,可以通过插桩获取程序的控制流(例如代码覆盖情况)和数据流(例如运行时变量值)信息。代表性的基于程序插桩的工具/框架有LLVM DataSanitizer、American Fuzzy Lop、SymCC等,对程序分析、软件安全测试具有重要意义。

程序插桩主要有三类方案,适用于不同的目标和场景:

  • 编译时插桩:适用于有源码的白盒情况,通常在中间语言上进行插桩,插桩的代码能够从编译优化中收益,是开销最低的插桩方案。
  • 动态二进制插桩:适用于无源码的黑盒情况。大多数动态二进制插桩框架都有三种执行模式:解释模式( Interpretation mode)、探测模式(probe mode)和JIT模式(just-in-time mode)。JIT模式是最常用的实现方式,例如Intel Pin。但同时,相比编译时插桩,无论解释还是JIT都引入了更多的额外开销。
  • 静态二进制插桩:适用于无源码的黑盒情况。静态二进制插桩主要是通过二进制重写技术来实现的,即在汇编语言的级别上进行插桩,插桩的代码是直接写入目标程序的二进制文件中的。静态二进制插桩的效率通常认为高于动态二进制插桩,但低于编译插桩。目前Linux平台上二进制重写技术比较成熟,但windows平台上的静态二进制插桩框架还比较少(WinAFL就应用了静态二进制插桩技术来提升效率)。

程序插桩技术广泛应用于程序分析/自动化漏洞挖掘研究与实践中,例如:

  • 基于覆盖率反馈的模糊测试(AFL)
  • 污点分析(LLVM Dsan, Angora)
  • 符号执行(SymCC)

LLVM框架在编译时允许用户编写自己的LLVM Pass,在中间语言(LLVM IR)级别进行程序插桩,结合LLVM IR提供的丰富程序信息,可以结合静态分析更好地进行插桩。

个人认为学习LLVM编译插桩最好的途径是找到应用该技术的一些开源项目,例如AFL,结合官方文档进行学习。以下,仅分享一些我们之前工作中用到的有意思的API的示例代码

2. 插桩分支代码:BranchInst

2.1 splitblockandinsertifthenelse()

stackoverflow

2.2 SplitBlockAndInsertIfThen()

或者仅仅想插桩if ... then ...的逻辑,就可以用SplitBlockAndInsertIfThen(),其使用相对简单些,一个例子如下:

Value``*` `val_c ``=` `NULL;``IRBuilder<> IRB(InsertPoint);``Value``*` `cmp` `=` `IRB.CreateICmpEQ(val_a, val_b);``BranchInst ``*``BI ``=` `cast<BranchInst>(``   ``SplitBlockAndInsertIfThen(``cmp``, InsertPoint, false, CallWeights));``/``*` `Instrument at new basic block ``*``/``IRBuilder<> ThenB(BI);``val_c ``=` `ThenB.CreateAdd(val_a, val_b);``val_c ``=` `IRB.CreateSub(val_a, val_b);

上述插桩将在目标程序中插入如下代码:

if` `(val_a ``=``=` `val_b) {``   ``val_c ``=` `val_a ``+` `val_b;``}``val_c ``=` `val_a ``-` `val_b;

值得注意的是,splitblockandinsertifthenelse()的第三个参数可以由用户可选地提供一个branch weight的参数,指定该分支的通过概率以便于进行优化(类似unlikely?)。

3. getIntNTy()

LLVM IR中惯用的IntegerType主要是:

  • Int8Ty
  • Int16Ty
  • Int32Ty
  • Int64Ty

但是,今天注意到了一个有意思的API:

static IntegerType ``*`   `getIntNTy (LLVMContext &C, unsigned N)

从而,我们可以定义任意长度的IntegerType,能够更加灵活地使用LLVM玩出花样。以下给出一个例子,在这个例子中,我们希望提取字符串的前N字节(N <= 8)。

3.1 一个小练习

原始程序demo.c:

unsigned long long str2hex(char *buf) {
      return 0;
}
int main() {
      char buf[32];
      scanf("%s", buf);
      printf("Hex: %8llx\n", str2hex(buf));
      return 0;
}

我们的目标是插桩democ.c的str2hex()函数,使该函数返回char *buf的前N = 6个字节的Hex形式。

为了实现该目标,我们需要编写一个LLVM Pass,其中主要逻辑如下:

for (llvm::Function &F : M) {
      if (F.getName() != "str2hex") continue;
      IntegerType *IntNTy = IntegerType::getIntNTy(C, 48); // 6 * 8 = 48 bits
      IntegerType *Int64Ty = IntegerType::getInt64Ty(C);
      BasicBlock::iterator IP = F.getEntryBlock().getFirstInsertionPt();
      Value* arg = &(*F.arg_begin());
      IRBuilder<> IRB(&(*IP));
      Value *ptr = IRB.CreateBitCast(IRB.CreateGEP(arg, ConstantInt::get(Int64Ty, 0)), IntNTy->getPointerTo());
      Value *hex = IRB.CreateLoad(ptr);
      hex = IRB.CreateZExt(hex, Int64Ty);
      for (auto &I : *IP->getParent()) {
            if (dyn_cast<llvm::ReturnInst>(&I)) {
                  llvm::ReturnInst *new_inst = llvm::ReturnInst::Create(m->getContext(), hex);
                  ReplaceInstWithInst(&I, new_inst);
                  break;
            }c
      }
}

插桩后的demo.ll如下:

define i64 @str2hex(i8*) #3 {
  %2 = getelementptr i8, i8* %0, i64 0
  %3 = bitcast i8* %2 to i48*
  %4 = load i48, i48* %3
  %5 = zext i48 %4 to i64
  %6 = alloca i8*, align 8
  store i8* %0, i8** %6, align 8
  ret i64 %5
}

3.2 IntNTy在x86上的实现方式

之所以LLVM IR中的Int8TyInt16TyInt32Ty更常用,是因为它们对应了ByteWordDword等类型,可以直接通过汇编指令表示。为了探究IntNTy,即任意长度的整型在x86汇编中的实现,我们将上一小节中的demo.c编译成可执行文件,并通过gdb反汇编来查看插桩代码的实现:

push   rbp
mov    rbp,rsp
mov    eax,DWORD PTR [rdi]
mov    ecx,eax
movzx  eax,WORD PTR [rdi+0x4]
mov    edx,eax
shl    rdx,0x20
or     rcx,rdx
mov    QWORD PTR [rbp-0x8],rdi
mov    rax,rcx
pop    rbp
ret

可以看到,对于6字节的IntNTy,汇编中首先读取一个4字节的Dword,接着读取一个2字节的Word,然后通过“移位”与“或”操作组合成一个6字节的值。

显然,IntNty提供了更方便的方式,让我们可以在插桩时使用一个任意长度的整型值。但是,从汇编角度,当LLVM IR最终编译成汇编代码时,IntNTy仍然是基于Byte、Dword等类型实现,并且仍然需要引入额外的开销来进行组合。LLVM IR只不过是将基础类型“组合”的部分进行了封装,提供了便捷但并不会提升效率。

4. Predecessors与Successors

类似IDAPython,在基本块的级别获取其前继/后继。例如:

for (llvm::BasicBlock* pred_bb : predecessors(cur_bb)) {
   printf("prev_bb=%p\n", pred_bb); /* for debug */
}

4.1 问题:predecessors()可能返回重复的前驱基本块

这里需要注意,我们遇到过一个奇怪的情况(通常发生在switch结构中),即predecessors()返回多个相同的基本块指针:

prev_bb=0x56407eaee190
prev_bb=0x56407eaee190
prev_bb=0x56407eaee190
prev_bb=0x56407eaee190
prev_bb=0x56407eaee190

Github Issue中有人遇到了相同的情况:#76. 目前,我们只能在调用predecessors()后手动进行检查和去重。

4.2 unique predecessor/successors

查看LLVM BasicBlock类的文档时,发现了一些有意思的成员函数

BasicBlock *getSingleSuccessor () const
     Return the successor of this block if it has a single successor. More...
 
BasicBlock *getUniqueSuccessor () const
     Return the successor of this block if it has a unique successor. More...

我们主要疑惑于其中"Unique"的含义。在查阅源码注释后,基本得到了解答:

/// Note that unique predecessor doesn't mean single edge, there can be
/// multiple edges from the unique predecessor to this block (for example a
/// switch statement with multiple cases having the same destination).

最后,本篇只是做一个简单的分享,欢迎批评与指正。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值