链接加载与ATT汇编
链接加载与AT&T汇编
水一水又一篇
用kali或者Ubuntu
文章目录
- 链接加载与AT&T汇编
- 1. 链接加载相关
- 1. gcc,readelf,objdump,ld,ldd
- 2. gdb
- 3. strace
- 4. ELF相关
- 5. 静态链接小测试
- 正常能跑的程序
- 不能正常跑的程序
- 6. 动态链接
- 基本款
- 豪华款(默认选项,使用过程链接表(PLT表))
- 2. 内联汇编(AT&T汇编)
- 参考
1. 链接加载相关
编译器gcc,汇编器as,链接器ld,调试器gdb,追踪器strace/ltrace,profiler(perf)
1. gcc,readelf,objdump,ld,ldd
gcc
-O2 编译优化级别设置为O2
-fno-pic 关闭位置无关代码(PIC)的生成
-g 生成调试信息,可用gdb进行调试
-c 生成.o文件(只编译不链接)
-l 指定头文件搜索路径
-L 指定库文件搜索路径
-m32 生成32位程序
-static 静态链接
-v 这个选项会显示编译器的版本信息以及编译和链接过程中的详细信息
-z execstack 启用可执行堆栈
-fno-stack-protector 禁用栈保护
-no-pie 关闭PIE保护
添加Toolchain Test Builds PPA(推荐,以获取最新版GCC):
sudo add-apt-repository ppa:ubuntu-toolchain-r/test
sudo apt update
安装新版本的GCC(例如安装GCC 7):
sudo apt install gcc-7 g++-7
添加gcc和g++版本条目:
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 60 --slave /usr/bin/g++ g++ /usr/bin/g++-5
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 60 --slave /usr/bin/g++ g++ /usr/bin/g++-7
如果有多个GCC版本,设定命令的默认版本:
sudo update-alternatives --config gcc
readelf -a main
用于显示ELF文件的所有信息,包括二进制和调试信息。
-h:文件头
-S:段表
-s:符号表
-d: 查看依赖库
-p:查看某个段内容,非常重要。如:readelf -p .comment libc.so (通过-p对只读段的查看就可以替代strings命令)
objdump -d main.o
反汇编程序
-d:反汇编所有代码段
-D:在-d基础上还包括符号表和调试信息
-h:显示文件节头信息
-r:显示重定位表信息
-t:显示BSS段内容
–s:代码段、数据段、只读数据段,各个段二进制
-a:看一个.a静态库文件中包含了哪些目标文件
ld a.o b.o c.o
-e:程序从某个函数开始。
-o:指定输出文件的名称。
-l:链接指定的库文件。
-L:添加库搜索路径。
-r:生成可重定位的输出。
-noinhibit-exec:忽略非致命错误。
-v:显示链接信息。
ldd ./a.out
显示一个可执行文件或者共享库(动态链接库)所依赖的共享库,本质是个脚本vim $(which ldd)
2. gdb
gcc编译时须带-g
,不然没有行号信息。
命令 | 含义 |
---|---|
start | 开始执行 |
r | 重新执行 |
n | 单步步过 |
ni | 单步步过(汇编级别) |
s | 单步步入(C语言级别) |
si | 单步步入(汇编级别) |
c | 继续运行 |
q | 退出 |
b | 设置断点 普通断点: b file.c:行号 或者b main 或b 行号 条件断点: b file.c:行号 if num == 2 查看断点: info b 删除断点: del 2 禁用启用: disable / enable 2 |
watch | 观察断点 监控指针 *p :watch *p 监控数组 a[10] :watch a 监控局部变量num: watch num |
catch | 捕捉断点 |
clear | 删除断点 |
bt | 查看函数调用堆栈信息 |
display | 查看变量值display num 程序每暂停一次,自动打印变量num的值 启用/禁用: disable / enable display num |
list | 默认显示当前行的上5行和下5行的源代码 |
打印
p var 打印变量的值
p &var 打印变量地址
p *addr 打印所指向的值
p /x var 十六进制显示值
用gdb查看内存
一般格式: x /nfu
x/50x $rsp
打印堆栈前50个内存单元的内容,每个单元默认占用4个字节
说明
x 是 examine 的缩写
n表示要显示的内存单元的个数
f表示显示方式, 可取如下值
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
f 按浮点数格式显示变量。
c 按字符格式显示变量。
i 指令地址格式
a 按十六进制格式显示变量。
u表示一个地址单元的长度
b表示单字节,
h表示双字节,
w表示四字节,
g表示八字节
其他格式指令
`x/s 地址` 用于显示内存中的字符串
x/10s $esp-144
#$esp-144处的内存以字符串形式显示
`x/i 地址` 命令用于显示内存中的指令
图形化调试界面
layout src 显示源代码窗口
layout asm 显示汇编代码窗口
layout regs 显示寄存器窗口
3. strace
跟踪系统调用和接收的信号
strace -o output.txt -T -tt -e trace=all -p 28979
#跟踪28979进程的所有系统调用(-e trace=all),并统计系统调用的花费时间-T,以及开始时间-tt(并以可视化的时分秒格式显示),最后将记录结果存在output.txt文件里面。
strace -f -F -o ./output.txt dcopserver
#这里 -f -F选项告诉strace同时跟踪fork和vfork出来的进程,-o选项把所有strace输出写到./output.txt里面,dcopserver是要启动和调试的程序。
4. ELF相关
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件 (Relocatable File) | 这类文件包括代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可归为这一类 | Linux的.o Windows的.obj |
可执行文件 (Executable File) | 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件 | /bin/bash,a.out Windows的.exe |
共享目标文件 (Shared Object File) | 这种文件包含了代码和数据,可以在下面两种情况下使用: 1.链接器使用此文件和其他可重定位文件和共享目标文件链接,产生新的目标文件; 2.动态链接器将几种共享目标文件与可执行文件结合,作为进程映像的一部分运行 | Linu的.so(E. g.:/lib/glibc-2.5.so windows的DLL |
核心转储文件 (Core Dump File) | 当进程意外终止时,系统可以将该进程地址空闲的内容及终止时的一些信息转储到此 | linux下的core dump |
.text 代码节
.rodata 只读数据节
.data 已初始化的全局变量或静态变量
.bss 指未初始化(或初始化为0)的全局变量或静态变量
.symtab 符号表
.rel 重定位信息
section header table 节头表
5. 静态链接小测试
vim a.c
int foo(int a,int b){
return a+b;
}
vim b.c
int x=100,y=200;
vim main.c
extern int x,y;
int foo(int a,int b);
int main(){
printf("%d+%d=%d\n",x,y,foo(x,y));
return 0;
}
正常能跑的程序
gcc -O2 -fno-pic -c ./a.c
gcc -O2 -fno-pic -c ./b.c
gcc -O2 -fno-pic -c ./main.c
gcc -static a.o b.o main.o
./a.out
strace ./a.out
readelf -h ./a.out
objdump -D ./main.o
可以看到链接前call(e8)后的地址为空
readelf -a main.o
可以看到左侧0x11
也表示我们要填函数地址的地方(e8后四字节)。
为什么call xxx
中xxx
填写的foo的地址要减4?
因为call xxx
指令长度不固定,所以xxx
的偏移是相对于call xxx
的下一条指令的偏移
objdump -D ./a.out
readelf -a ./a.out
可以看到符号表的0x60019c
表示y
,并且注意到foo的地址为0x400126
,刚好等于0x0021
+0x400105
不能正常跑的程序
gcc -O2 -fno-pic -c ./a.c
gcc -O2 -fno-pic -c ./b.c
gcc -O2 -fno-pic -c ./main.c
ld a.o b.o main.o -noinhibit-exec
./a.out
段错误
gdb ./a.out
start
可以看到printf
这里变成了call 0x0
,访问了非法地址。
ld只链接了a.o,b.o,c.o,gcc还链接了标准库相关的东西
6. 动态链接
https://ysyx.oscc.cc/slides/hello-x86.html
ELF查表
基本款
call *table[printf]
在链接时,填入运行时的table
使用 -fno-plt
选项开启(不要在位置无关的代码中使用 PLT 进行外部函数调用。相反,在调用站点从 GOT(全局偏移表) 加载被调用者地址并跳转到该地址。)
gcc版本不要太低
gcc -c -fno-plt ./a.c
gcc -c -fno-plt ./b.c
gcc -c -fno-plt ./main.c
gcc -fno-plt a.o b.o main.o
豪华款(默认选项,使用过程链接表(PLT表))
Procedure Linkage Table
调用printf函数的过程
gcc默认将printf
优化成puts
但下文仍然采用对printf
的介绍
-
通过gcc编译链接的程序默认采用延迟绑定技术,对
printf
的调用会跳转到PLT表项printf@plt
, -
该表项为一个桩函数,将跳转到对应的GOT表项所指的位置
-
由于进程初次调用printf()函数,该GOT表项默认指向PLT[0],PLT[0]跳转到位于GOT[2]的动态链接器延迟绑定函数
_dl_runtime_resolve()
, -
_dl_runtime_resolve()
将获取printf()函数的实际地址,将该地址写入到printf对应的GOT表项中(后续对printf()
的调用可通过上述桩函数直接进入printf()
函数执行),跳转到printf()执行,printf()函数最终将调用系统级I/O函数write(1,"hello world\n",13)
printf@plt:
jmp *table[PRINTF]
push $PRINTF
call resolve
gcc -c ./a.c
gcc -c ./b.c
gcc -c ./main.c
gcc a.o b.o main.o
/lib64/ld-linux-x86-64.so.2 ./a.out
是./a.out
真正再操作系统里的行为,这与Sha-Bang(#!)的实现机制相同
初次调用
再次调用
2. 内联汇编(AT&T汇编)
注意内联汇编与x86汇编区别
内联汇编目的操作数在右边,x86汇编目的操作数在左边
add 0x4(%esp),%eax ;add eax,[esp+0x4]
lea (%rdi,%rsi,1),%eax ;lea eax,[rdi+rsi*1]
立即数 $13
,$0x80
寄存器 %rax
,实际写代码要多加一个%
标签 %l[label1]
换行 \n\t
%0
一般表示第一个用到的寄存器
寻址方式
100
//访问当前段:100处内存
%es:100
//访问es:100处内存
(%eax)
//访问当前段:eax指向内存,类似于[eax]
(%eax,%ebx)
//访问当前段:(eax+ebx)处内存
(%ecx,%ebx,2)
//访问当前段:(ecx+ebx*2)处内存
(,%ebx,2)
//访问当前段:(ebx*2)处内存
-10(%eax)
//访问当前段:(eax-10)处内存,类似于[eax-10]
%ds:-10(%ebp)
//访问ds:(ebp-10)处内存
add -0x4(%esp),%eax
//add eax,[esp-0x4]
jmp *%rax //用寄存器%rax中的值作为跳转目标
jmp *(%rax) //以%rax中的值作为读地址,在从内存中读出跳转目标
助记符后缀
b:1个字节
w:2个字节
l:4个字节
q:8个字节
比如:movl $100,%es:(%eax)
cpp书写格式
asm(assembler template
:output operands(optional)
:input operands(optional)
:list of clobbers registers(optional) //需要使用的寄存器
);
asm goto(
assembler template
:output operands(optional)
:input operands(optional)
:list of clobbers registers(optional) //需要使用的寄存器
:gotolabels //实现汇编的跳转
);
asm volatile() //不做编译优化
[[asmSymbolicName]] constraint (cvariablename)
constraint:
m 内存
r 寄存器
i 立即数
constraint modifier characters 对于output是必须的
= 只写
+ 读写
#include <stdio.h>
int inc1(int src) {
int dst;
asm volatile("mov %1,%0\n\t" //%0表示dst(eax),%1表示src
"add $1,%0"
:"=r"(dst)
: "r"(src));
return dst;
}
int inc2(int src) {
int dst;
asm volatile("mov %1,%0\n\t" //%0表示dst(eax),%1表示src
"mov $3,%%eax\n\t"
"add $1,%0"
:"=r"(dst)
: "r"(src));
return dst;
}
int inc3(int src){
int dst;
asm volatile("mov %1,%0\n\t"
//%0表示dst(edx),%1表示src
"mov $3,%%eax\n\t"
"add $1,%0"
:"=r"(dst)
:"r"(src)
:"%eax");
return dst;
}
int main(int argc, char* argv[]) {
printf("inc1:%d\n", inc1(1));
printf("inc2:%d\n", inc2(1));
printf("inc3:%d\n", inc3(1));
return 0;
}
编译链接一下
gcc ./test1.c
./a.out
反汇编
objdump -d ./a.out
可以看到第三个函数相较于第二个函数使用了edx做加法,最后再给回eax作为返回值,避免了eax复用导致的问题。
参考
链接与加载 why_hy_y