零.阅读目的
C++开发的游戏服务器避免不了偶尔出现的宕机问题,在查找宕机问题时,一般都会分析dump,但由于编译器优化问题和64位dump调试的不方便,能看懂汇编可以起到事半功倍的效果,通常可以通过反汇编查找空指针或者程序的执行过程,所以阅读本书的目标是:看懂汇编,并不深究。
一.汇编基础
1.基础指令汇总
- mov //传送指令
- cmov //条件传送指令
- xchg //交换指令
- push //压栈
- pop //出栈
- pusha/popa //压入/弹出所有16位通用寄存器
- pushad/popad //压入/弹出所有32位通用寄存器
- add //加法
- sub //减法
- inc //自增
- dec //递减
- mul //无符号乘
- imul //带符号乘
- div //无符号除
- idiv //带符号除
- sal //向左移位,右边补0
- shr //无符号,向右移位,左边补0
- sar //带符号,向右移位,左边补1
- lea //赋值地址
- xor //异或,可用来清零 ^
- or //或者 ||
- not //非 !
- and //并且 &&
- nop //空指令
- test //位判断
- je //j开头的均为条件跳转指令,带n的为反义
- call //调用函数
- enter //替代函数操作esp,pushl %ebp movl %esp, %ebp
- leave //替代函数操作esp,movl %ebp, %esp popl %ebp
- ret //函数返回指令
- jmp //跳转到某个地址
- int //中断
- rep //重复执行某个操作,知道ecx为0
- loop //循环直到ecx寄存器为0
以上列举了一些非常常见的汇编指令,在调试过程中,这些指令无处不在,也是必须要掌握的基本指令。
2.数据类型
- AT&T语法
使用L,W,B来表示数据大小,分别代表四位long,两位word,一位byte; - intel语法
byte(字节)、word(字)、dword(双字)、qword(四字)、tbyte(十字节),可以放在ptr前面
二.通用寄存器
1.32位寄存器
- EAX 用于操作数和结果数据的累加器
- EBX 指向数据内存段中的数据的指针
- ECX 字符串和循环操作的计数器
- EDX I/O指针
- EDI 用于字符串操作的目标的数据指针
- ESI 用于字符串操作的源的数据指针
- ESP 堆栈指针
- EBP 堆栈数据指针
2.64位寄存器
- RAX
- RBX
- RCX
- RDX
- RDI
- RSI
- RSP
- RBP
3.系统寄存器
- EIP 系统寄存器,用来记录CPU要执行的指令地址
4.寄存器的特定使用
linux程序中,程序的退出的状态码保存在%ebx寄存器中
movl $8, %ebx
echo $? //可以显示上一个程序的退出码,也就是ebx寄存器的值
linux平台可以使用echo $?查看程序返回值,若使用汇编变成,那么可以将返回值传送到ebx寄存器
5.8位、16位、32位寄存器
位数 | 寄存器 | 寄存器 | 寄存器 | 寄存器 |
---|---|---|---|---|
32位 | EAX | EBX | ECX | EDX |
16位 | AX | BX | CX | DX |
8位 | AH/AL | BH/BL | CH/CL | DH/DL |
三.开发工具
1.汇编器
MASM 微软开发的 http://www.masm32.com/
NASM
GAS GNU系列,另外有gcc、g++
HLA
2.连接器
ld:把汇编语言目标代码和其他库连接在一起,生成操作系统可执行文件
3.调试器
gdb:停止程序、检查修改数据
4.编译器
as:把高级语言转换为处理器能够执行的指令码
5.目标代码反汇编器
objdump:将可执行文件或者目标代码文件转换成汇编语言
6.简档器
gprof:跟踪每个函数在程序执行过程中被使用时花费了多长处理器时间
7.一些需要用到的工具
gdb
kdbg 图形化调试工具
objdump 查看反汇编
gprof 性能分析工具:可以查看函数被调用多少次多少时间
gcc -o demo demo.c -pg
./demo
gprof demo > gprof.txt
gcc过程:
gcc -S ctest.c //生成ctest.s
as ctest.s -o ctest.o
ld ctest.o -o ctest //这一步如果涉及到调用C库函数,那么就得带其他参数
四.操作码语法(Intel和AT&T的语法不同)
AT&T和intel汇编语法,比较明显的是操作数顺序相反,主要区别有以下几点:
编号 | Intel | AT&T | AT&T说明 |
---|---|---|---|
1 | 4 | $4 | AT&T使用$表示立即操作数 |
2 | eax | %eax | AT&T在寄存器名称前面加上前缀% |
3 | mov eax, 4 | movl $4, %eax | 处理源和目标使用相反的顺序 |
4 | mov eax, dword ptr test | movl $test, %eax | AT&T不用指定数据长度,但mov后面要指定L,W,B |
5 | jmp section:offset | ljmp
|
长调用和跳转使用不同语法定义段和偏移值 |
6 | -4(%ebp) | [ebp-4] | 间接寻址 |
7 | foo(,%eax,4) | [foo + eax*4] | 间接寻址 |
这里只是列举了几个常见并且比较基本的区别,太复杂的语法没有深究。
五.汇编程序
1.基本模板
#注释
.section .data
.section .bss
.section .text
.globl _start
_start:
movl $0, %eax
2.编译
as cpuid.s -o cpuid.o (-gstabs 添加调试信息)
ld cpuid.o -o cpuid
3.调试(几个gdb常用调试命令)
- break * label + offset 下断点,指定行数也行
- next 下一行
- step 下一步,若有函数,则进入函数
- continue
- run
- info registers 查看寄存器
- print /x $ebx 查看寄存器十六进制值
- layout asm 切到反汇编
- x/nyz: 显示内存位置
- n是字段数: 个数
- y是输出格式 c字符 d十进制 x十六进制
- z是显示字段长度 b字节 h半字 w32位字
- 例子:x/42cb &output 查看该变量42位字符
4.测试程序
#cpuid2.s
.section .data
output:
.asciz "The processor Vendor ID is '%s'\n"
.section .bss
.lcomm buffer, 12
.section .text
.globl _start
_start:
movl $0, %eax
cpuid
movl $buffer, %edi
movl %ebx, (%edi)
movl %edx, 4(%edi)
movl %ecx, 8(%edi)
pushl $buffer
pushl $output
call printf
addl $8, %esp
pushl $0
call exit
编译运行
gzshun@gzshun-vm:~/c$ as cpuid2.s -o cpuid2.o
gzshun@gzshun-vm:~/c$ ld cpuid2.o -o cpuid2 -lc --dynamic-linker /lib/ld-linux.so.2
gzshun@gzshun-vm:~/c$ ./cpuid2
The processor Vendor ID is 'GenuineIntel'
一开始我阅读本书的时候,对汇编语法并不是很熟,其中犯了一个低级错误,代码看了很久却始终找不出问题,有2个地方:
1.%ebx 错写成 $ebx
在AT&T语法中,寄存器前面要使用百分号%,而我写成$号不能解析。
gzshun@gzshun-vm:~/c$ ld cpuid2.o -o cpuid2 -lc --dynamic-linker /lib/ld-linux.so.2
cpuid2.o: In function `_start':
(.text+0xe): undefined reference to `ebx'
2.(%edi) 错写成 %edi
一开始我怀疑是作者写的程序适用于比较古老的操作系统,然后测试了centos、ubuntu和redhat几个系统都不行,但是运行结果都是得到这样的结果:segmentation fault (core dumped),后来我就没管了。在读完本书后,我再回头看这个程序的出错原因,通过gdb调试,才知道原来是%edi没带括号,导致movl时把edi指针给破坏掉,其实作者的本意是修改edi指针指向的数据。
gzshun@gzshun-vm:~/c$ as cpuid2.s -o cpuid2.o -gstabs
gzshun@gzshun-vm:~/c$ ld cpuid2.o -o cpuid2 -lc --dynamic-linker /lib/ld-linux.so.2
gzshun@gzshun-vm:~/c$ gdb cpuid2
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
(gdb) b 13
Breakpoint 1 at 0x80481cc: file cpuid2.s, line 13.
(gdb) r
Starting program: /home/gzshun/c/cpuid2
Breakpoint 1, _start () at cpuid2.s:13
13 movl %ebx, %edi
(gdb) x/12c buffer
0xb7fbd5d4 <buffer>: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
0xb7fbd5dc <buffer+8>: 0 '\000' 0 '\000' 0 '\000' 0 '\000'
(gdb) x/12c $edi
0x80492c8 <buffer>: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
0x80492d0 <buffer+8>: 0 '\000' 0 '\000' 0 '\000' 0 '\000'
(gdb) s
14 movl %edx, 4(%edi)
(gdb) x/12c $edi
0x756e6547: Cannot access memory at address 0x756e6547
(gdb)
从调试的打印信息可以看出,在执行完movl %ebx, %edi后,edi寄存器的值就被修改了。
5.数据类型
汇编语言的数据类型用于声明程序中的变量,跟C语言的类型差不多。
命令 | 数据类型 |
---|---|
.ascii | 字符串 |
.asciz | 空字符结尾的字符串 |
.byte | 字节值 |
.double | 双精度浮点数 |
.float | 单精度浮点数 |
.int | 32位整数 |
.long | 同.int |
.octa | 16字节整数 |
.quad | 8字节整数,也就是64位 |
.short | 16位整数 |
.single | 同.float |
6.数组
- 声明
sizes:
.int 100,150,200,250,300 - 访问
movl $sizes, %edi
movl $2, %edi
movl values(, %edi, 4), %eax //eax寄存器的值被写错200,也就是sizes[2]
7.bss段
命令 | 描述 |
---|---|
.comm | 声明未初始化的数据的通用内存区域 |
.lcomm | 声明未初始化的数据的本地通用内存区域,会占用程序空间 |
汇编程序中,如果在.lcomm标签定义一个很大的数组,那么生成的程序的大小就会包括数组的大小。
六.指令集语法
1.传送数据
传送数据
movl value, %eax //把value的数据值传送给eax寄存器
movl %ecx, value //把ecx寄存器数据传送给value使用变址的内存位置(访问数组)
base_address (offset_address, index, size)
movl $2, %edi
movl values(, %edi, 4), %eax //访问values数组,以大小为4字节,偏移为2个单位的数据,也就是C语言中的values[2]使用寄存器间接寻址
movl $values, %edi //将values的内存地址传送到edi寄存器
movl $ebx, (%edi) //将ebx寄存器的值传送到edi寄存器中包含的内存位置,也就是指针
movl %edx, 4(%edi) //edi指针后面的4个字节
movl %edx, -4(%edi) //edi指针前面的4个字节-
条件传送指令
cmovx source, destination
比较结果存在EFLAGS寄存器中,有带符号、无符号之间的比较:
下表是无符号条件传送指令:指令对 描述 EFLAGS状态 CMOVA/CMOVNBE 大于/不小于或者等于 (CF或ZF)=0 CMOVAE/CMOVNB 大于或者等于/不小于 CF=0 CMOVNC 无进位 CF=0 CMOVC 进位 CF=1 CMOVB/CMOVNAE 小于/不大于或者等于 CF=1 CMOVBE/CMOVNA 小于或者等于/不大于 (CF或ZF)=1 CMOVE/CMOVZ 等于/零 ZF=1 CMOVNE/COMVNZ 不等于/不为零 ZF=0 CMOVP/CMOVPE 奇偶校验/偶校验 PF=1 CMOVNP/CMOVPO 非奇偶校验/奇校验 PF=0 从上表可以看出,无符号条件传送指令依靠进位、零和奇偶校验标志确定两个操作数之间的区别。
上面的指令有些是用 / 符号隔开指令对,这两个指令具有相同的含义。比如,一个值大于另外一个值,也可以说是不小于或者等于另外一个值。这两个条件是等同的,但是二者具有个字的传送指令,如 CMOVA 和 CMOVNBE .
如果操作数是带符号值的,就必须使用不同的条件传送指令集,如下表所示:指令对 描述 EFLAGS状态 CMOVGE/CMOVNL 大于或者等于/不小于 (SF异或OF)=0 CMOVL/CMOVNGE 小于/不大于或者等于 (SF异或OF)=1 CMOVLE/CMOVNG 小于或者等于/不大于 ((SF异或OF) 或 ZF)=1 CMOVO 溢出 OF=1 CMOVNO 未溢出 OF=0 CMOVS 带符号(负) SF=1 CMOVNS 无符号(非负) SF=0
2.交换数据
指令 | 描述 |
---|---|
XCHG | xchg src, dst // src和dst交换 |
BSWAP | 0x12345678,转换后变成0x78563412 |
XADD | xadd src,dst // dst = src + dst |
CMPXCHG | cmpxchg src, dst //dst与eax比较,若相等则src传送到dst |
CMPXCHG8B | cmpxchg8b dst //将dst和edx:eax比较,若相等则将ecx:ebx传送到dst,高位:低位 |
3.堆栈
堆栈的地址是向下增长,esp寄存器存储栈顶指针,会随着push压入新数据而递减。
指令
- pushl source //压入堆栈
- pushw source
- popl dest //弹出堆栈
- popw dest
- pusha/popa //压入/弹出所有16位通用寄存器
- pushad/popad //压入/弹出所有32位通用寄存器
- pushf/popf //压入/弹出EFLAGS寄存器的低16位
- pushfd/popfd //压入/弹出EFLAGS寄存器的全部32位
栈顺序(从高地址向低地址增长)
intel处理器x86系列系统进程,栈向下,堆向上。
8051的栈是向高地址增长,INTEL的8031、8032、8048、8051系列使用向高地址增长的堆栈;但同样是INTEL,在x86系列中全部使用向低地址增长的堆栈。其他公司的CPU中除ARM的结构提供向高地址增长的堆栈选项外,多数都是使用向低地址增长的堆栈。
历史遗留
在没有MMU(Memory Management Unit/内存管理单元)的时代,为了最大的利用内存空间,堆和栈被设计为从两端相向生长。那么哪一个向上,哪一个向下呢?
人们对数据访问是习惯于向上的,比如你在堆中new一个数组,是习惯于把低元素放到低地址,把高位放到高地址,所以堆向上生长比较符合习惯。而栈则对方向不敏感,一般对栈的操作只有push和pop,无所谓向上向下,所以就把堆放在了低端,把栈放在了高端。MMU出来后就无所谓了,只不过也没必要改了。
函数参数会涉及到堆栈知识,文章中的函数章节会详细讲解C样式函数堆栈。
4.控制执行流程
函数调用
- jmp location //跳转指令
- call address //调用函数
- enter == push %ebp mov %esp,%ebp
- leave == mov %ebp,%esp pop %ebp
跳转指令
jxx address,跳转指令非常多,都是j开头的指令:
- ja/jg // > above greater
- jae/jge // >=
- jb/jl // < below less
- jbe/jle // <=
- je //==
- jz //==0 zero
- js //带符号 sign
- jo //溢出 over
- jp //奇偶校验
- jcxz //cx寄存器为0就跳转
- jecxz //ecx寄存器为0就跳转
- jnx //jn开头的跳转指令与上面的指令意义相反,这里就不列出了。
上面指令有ja/jg和jb/jl两种重复的意义,但是用法却不同,ja/jb(above/below)用于无符号数值判断,jg/jl(greater/less)用于带符号数值判断。
比较指令
cmp a, b //内部处理:b - a
jge address //此时如果调用jge,在b > a的情况下才会跳转
循环指令
loop address //循环直到ecx寄存器为0
若ecx为0,会导致loop问题,所以使用jcxz/jecxz,在ecx==0的情况下跳转
5.数字
整数长度
byte word doubleword quadword 与C语言类似
字节顺序
注意:内存数据是小端格式(little-endian),寄存器是大端格式(big-endian) ,使用gdb的x/4b &data可以查看字节顺序
传送不同数据大小的数字
假设如果要从16位数字传送给32位寄存器,那么要先将高位设置成0:
movl $0, %ebx //这一行也可以用异或代替:xor %ebx
movw %ax, %ebx
intel提供一个命令替代上面的操作:
movzx source, dest //无符号:根据source的位数大小,只拷贝这一部分到dest的低位,其他位设置成0
movsx source, dest //带符号:传送带符号整数,除了拷贝低位,其他位设置成1
MMX整数
movq source, dest //将数据传送到MMX寄存器中,比如%mm0,%mm1
SSE整数
movdqa source, dest //将数据传送到XMM寄存器中,比如%xmm0, %xmm1
其他
- 原码:数据本身
- 反码:原码的取反
- 补码:反码+1
浮点数
- 科学计数法
0.159 * 10^0 值这样算:0 + (1/10) + (5/100) + (9/1000) //这是日常人类看得懂的数字 - 二进制浮点数
1.0101 * 2^2 是 101.01,值这样算:5 + (0/2) + (1/4) = 5.25 //这是计算机看得懂的 -
例子
二进制 十进制分数 十进制值 0.1 1/2 0.5 0.01 1/4 0.25 0.001 1/8 0.125 -
二进制浮点格式
浮点类型 符号位 指数 系数(有效数字) float 31 23~30 0~22 double 63 52~62 0~51 float的有效数字有23位,那么2^23=8388608,结果的长度是7位数字,所以float的精度是7位小数点。
double的有效数字有52位,那么2^52=4503599627370496,结果的长度是16位,所以double的精度是16位小数点。
浮点数指令
F开头的指令基本上是浮点数的操作指令,大概了解一下就行。
- FLD source //会将浮点数压入FPU堆栈,st0、st1
- FLDS source //单精度
- FLDL source //双精度
- FLD1 //加载1
- FLDL2T //log
- FLDL2E //log
- FLDPI //压入3.1415926
- FLDLG2 //log2
- FLDLN2 //ln
- FLDZ //0.0
SSE浮点
MOVAPS MOVUPS MOVSS MOVLPS MOVHPS MOVLHPS MOVHLPS
这些MMX,SSE高级数据对调试没什么帮助,就不深入学习。
6.基本数学功能
- 加法(b、w、l):
add source, dest - 双字加法(b、w、l):会将进位标志带入高位计算
adc source, dest - 减法(b、w、l):
sub source, dest - 双字减法(b、w、l):无符号的减法,考虑溢出和进位标志位
sbb source, dest - 递增/递减:无符号,不影响进位标志
inc dest
dec dest -
乘法(无符号):
mul source //无符号,目标数隐含,因为有以下情况源操作数长度 目标操作数 目标位置 8bit AL AX 16bit AX DX:AX 32bit EAX EDX:AX 乘法(带符号)
imul source //带符号,带符号要检查结果是否溢出,使用jo指令
imul source, dest
imul val, source, dest //dest = val * source-
除法(无符号)
div divisor //divisor是除数被除数 被除数长度 商 余数 AX 16bit AL AH DX:AX 32bit AX DX EDX:EAX 64bit EAX EDX 除法(带符号)
idiv divisor //带符号移位乘法:右边补0
sal(向左算术移位)/shl(向左逻辑移位)
sal dest //左移1位
sal %cl, dest //左移寄存器cl中的位数
sal val, dest //左移val位数移位除法
shr dest //无符号,左边补0
sar dest //带符号,左边补1循环移位:移位导致的溢出位放到值的另一端
rol dest //向左移位
ror dest //向右移位
rcl dest //向左移位,包含进位标志
rcr dest //向右移位,包含进位标志不打包BCD运算
AAA //在add后面
AAS //在sub后面
AAM //在mul后面
AAD //在div之前打包BCD运算:打包BCD值的是字节低4位放BCD低4位,字节高4位放BCD高4位
DAA //add或adc
DAS //sub或sbb布尔逻辑:
and source, dest
not source, dst
or source, dst
xor source, dst //异或,可用来清零
test source, dst //位测试,比如test $0x10, %eax清空进位标志:
clc
7.高级数学功能(FPU寄存器)
FPU寄存器寄存器为R0~R7,用来计算浮点型数据,一些操作指令跟基本的数学操作一样,只是前面多了一个F。
FPU寄存器堆栈
R0 –> ST7
R1 –> ST6
…
R7 –> ST0常用的浮点计算指令
fadd
fdiv
fdivr
fmul
fsub
fsubr三角函数:
fcos
fsin
fptan
三角函数、对数、平方、绝对值等等
fpu指令暂时没用到,不再深究。
8.处理字符串
传送字符串
movs(b,w,l):
隐含的源操作数是esi,隐含的目标操作数是edi,所以完整的指令是这样:
movs %esi, %edi //但是后面不用写,省略了,esi表示source,edi表示destination
例子:
movl $input, %esi
movl $output, %edi
movsl地址传送指令
lea output, %edi //该指令经常用来将内存地址赋值给destDF标志
每次执行movs指令,esi和edi会改变,若DF标志位0,则递增;若DF标志位1,则递减
cld //将DF清零
std //设置DF标志,注意,当向后处理字符串时,movs的指令仍然是向前获取内存REP前缀(repeat)
代替loop指令,根据ecx的值一直进行处理,直到ecx=0
ecx的长度要根据movsq,movsw,movsl的长度进行改变,若超出字符串边缘,会导致内存之后的数据也被读取到
rep //判断ecx
repe,repne,repnz,repz //判断ecx和ZF标志存储和加载字符串
lods //隐含的操作数是esi寄存器
lodsb //把一个字节加载到AL寄存器中
lodsw //2个字节到AX
lodsl //4个字节到EAX
stos //隐含的操作数是edi寄存器
stosb //AL
stosw //AX
stosl //EAX
lods和stos配合rep前缀,可以复制大型字符串值并处理,可以实现类似memset的功能比较字符串:
cmps(q,w,l)
隐含的参数是esi和edi,也可配合rep使用扫描字符串:
scas(b,w,l)
比较AL,AX,EAX和隐含edi寄存器的操作数,也可配合rep使用
七.函数
1.创建函数
固定格式如下:
.type fun1, @function
area:
ret
ret指令:执行ret,程序返回主程序,返回的位置是紧跟着call指令后面的指令
2.参数和返回结果
- 参数:寄存器、全局变量、堆栈
- 返回结果:寄存器、全局变量
3.调用函数
- 指令
call function - 参数
执行call之前,要把输入值放在正确的位置。在函数内部,可能会改变寄存器值,为了确保函数返回后可以恢复寄存器的状态,可以使用pusha和popa来保证,也可以针对特定的寄存器进行操作。
pusha //同时保存所有寄存器
popa //同时恢复所有寄存器
4.C样式传递数据值(堆栈)
- 参数顺序
C样式函数传参的解决方案是使用堆栈,参数的堆栈顺序与函数原型中的顺序相反 -
返回值
- 使用eax寄存器存储32位结果
- 使用edx:eax寄存器存储64位结果
- 使用FPU的ST0存储浮点值
esp
函数的开头和结尾,会保存esp,所以函数的格式一般如下:
function:
pushl %ebp
movl %esp, %ebp
...
movl %ebp, %esp
pop %ebp
ret
也可以这样写:
function:
enter
...
leave
ret
- 局部变量
在函数开头,通常会将esp减去一个偏移,这个是开辟了栈空间,函数call结束后,再对esp加了一个偏移,为了清空堆栈。
堆栈的数据使用%ebp指针进行间接寻址,比如-4(%ebp)是第一个局部变量(等下会讨论,这个不一定是第一个局部变量)。
function:
enter
subl $12, %esp
movl $1, -4(%ebp) #这里就有3个4字节的栈可以用
leave
ret
call fun1
addl $12, %esp #将刚才开辟的12个字节栈空间清掉
5.函数的堆栈空间
实验
用一个例子来讲解局部变量的栈顺序和函数的堆栈空间,在程序开始之前,先给个esp偏移地址对应的参数顺序,以下表格是模拟调用一个函数的堆栈空间,从上到下对应高地址到低地址:
地址 | 变量 | ebp偏移 |
---|---|---|
0xbfffefd4 | 函数参数3 | 16(%ebp) |
0xbfffefd0 | 函数参数2 | 12(%ebp) |
0xbfffefcc | 函数参数1 | 8(%ebp) |
0xbfffefc8 | 返回地址 | 4(%ebp) |
0xbfffefc4 | 旧的EBP值 | (%ebp) |
0xbfffefc0 | 局部变量3 | -4(%ebp) |
0xbfffefbc | 局部变量2 | -8(%ebp) |
0xbfffefb8 | 局部变量1 | -12(%ebp) |
我阅读了这本书的例子,还包括有网上一些堆栈空间的说明,对局部变量的栈顺序都有不同的理解,比如上面这个表格,局部变量1到底是对应-4(%ebp),还是对应-12(%ebp)呢?因为这个对于调试有时是有帮助的,有时想通过esp偏移量得到某个局部变量的大小或者内存,所以了解这个顺序是很有必要的。
于是使用以下程序进行验证:
开发环境:Ubuntu 16.04.1 LTS 32位
编译器:gcc version 5.4.0 20160609
#include <stdio.h>
int fun(int a, int b, int c)
{
int va = a;
int vb = b;
int vc = c;
return va;
}
int main()
{
int result = fun(1, 2, 3);
return 0;
}
使用gcc编译,用objdump进行反汇编查看对应的main函数和fun函数的汇编代码:
080483db <fun>:
80483db: 55 push %ebp
80483dc: 89 e5 mov %esp,%ebp
80483de: 83 ec 10 sub $0x10,%esp
80483e1: 8b 45 08 mov 0x8(%ebp),%eax
80483e4: 89 45 f4 mov %eax,-0xc(%ebp)
80483e7: 8b 45 0c mov 0xc(%ebp),%eax
80483ea: 89 45 f8 mov %eax,-0x8(%ebp)
80483ed: 8b 45 10 mov 0x10(%ebp),%eax
80483f0: 89 45 fc mov %eax,-0x4(%ebp)
80483f3: 8b 45 f4 mov -0xc(%ebp),%eax
80483f6: c9 leave
80483f7: c3 ret
080483f8 <main>:
80483f8: 55 push %ebp
80483f9: 89 e5 mov %esp,%ebp
80483fb: 83 ec 10 sub $0x10,%esp
80483fe: 6a 03 push $0x3
8048400: 6a 02 push $0x2
8048402: 6a 01 push $0x1
8048404: e8 d2 ff ff ff call 80483db <fun>
8048409: 83 c4 0c add $0xc,%esp
804840c: 89 45 fc mov %eax,-0x4(%ebp)
804840f: b8 00 00 00 00 mov $0x0,%eax
8048414: c9 leave
8048415: c3 ret
8048416: 66 90 xchg %ax,%ax
8048418: 66 90 xchg %ax,%ax
804841a: 66 90 xchg %ax,%ax
804841c: 66 90 xchg %ax,%ax
804841e: 66 90 xchg %ax,%ax
先看fun函数的汇编代码,其中有两行代码如下:
mov 0x8(%ebp),%eax
mov %eax,-0xc(%ebp)
0x8(%ebp)指向函数参数1,也就是a形参,先把a拷贝到eax寄存器,然后再拷贝到-0xc(%ebp),这时再看C代码,a形参是赋给va变量的,说明可以得出结论-0xc(%ebp)指向局部变量va,说明越靠后的局部变量的栈地址越靠近ebp,也就是说局部变量的声明顺序与栈空间顺序是相反的,得出表格如下:
地址 | 变量 | ebp偏移 |
---|---|---|
0xbfffefd4 | c | 16(%ebp) |
0xbfffefd0 | b | 12(%ebp) |
0xbfffefcc | a | 8(%ebp) |
0xbfffefc8 | 返回地址 | 4(%ebp) |
0xbfffefc4 | 旧的EBP值 | (%ebp) |
0xbfffefc0 | vc | -4(%ebp) |
0xbfffefbc | vb | -8(%ebp) |
0xbfffefb8 | va | -12(%ebp) |
示意图:
疑问
这里会有一个疑问,为什么vc变量的内存地址是排在ebp的下面(-4(%ebp))?
首先栈空间是从高地址往低地址增长,在函数内部,栈变量随着靠后的声明,内存地址会越来越高,当然越高的地址肯定是靠上,也就是接近ebp。特意写了个程序,把va和vb的内存地址打出来,va的地址小于vb的地址,结合栈空间的结构,可以说明栈顺序是这样:ebp -> vc -> vb -> va。
后来我在csdn发帖询问,结论是这样:不同平台不同编译器可能不一样,一般来说,Borland C++、ms VC++按照声明顺序从高地址向低地址排列,intel C++、gcc/g++则相反,按照声明顺序从低地址向高地址排列。
【原贴地址】
6.独立的函数文件
汇编程序的.globl标签一般是声明为_start,但是独立函数文件要声明为函数名称:
.section .text
.type area, @function
.globl area
area:
编译的话跟C语言编译一样,各自编译成.o文件,链接的时候使用ld统一将所有.o文件生成可执行程序
有些.s汇编文件要单步调试,有些不用,那就只需要在要调试的汇编文件使用-gstabs进行编译即可。
7.命令行参数
在Linux系统中,程序运行的虚拟内存地址从0x80480000开始,到地址0xbfffffff结束。所以调试程序时,可以看到地址开头一般都是8048,因为程序代码和数据放在8048那里,而堆栈数据放在bfffffff那里,esp指针指向bffffff。
8.系统调用
linux的系统调用函数对应的编号,可以参考unistd.h,里面内容如下:
#define __NR_exit 1 //exit函数对应的编号不一定是1
调用系统调用
movl $1, %eax //将系统调用编号写入eax
int 0x80 //使用int进行软中断系统调用的参数
eax用于存放函数编号
顺序:ebx(第1个参数) -> ecx -> edx -> esi -> edi统计字符串长度
output:
.ascii “helloworld”
output_len:
.equ len, output_len - output复杂的系统调用
比如传参是一个结构体指针,那么调用完函数会通过这个指针来获取数据,可以利用标签类声明结构体,每个标签对应一个变量,内存是连续的跟踪系统调用
strace //用来跟踪程序使用了哪些系统调用,返回值,使用时间等等
strace -p pid //动态附加
strace -c 程序 //使用时间
八.内联汇编
1.使用
在C/C++程序中,使用关键之asm,ANSI C使用asm包含的汇编程序
2.语法
asm("movl $1, %eax\n\t"
"movl $0, %ebx\n\t"
"int $0x80")
一定要使用换行符,制表符不是必须的,这种语法只能使用全局变量
编译后,汇编程序由#APP和#NO_APP包含
asm volatile (“”) //volatile防止编译器优化
例子:
#include <stdio.h>
int a = 2;
int b = 3;
int result = 0;
int main()
{
asm volatile ("pusha\n\t"
"movl a, %eax\n\t"
"movl b, %ebx\n\t"
"imull %ebx, %eax\n\t"
"movl %eax, result\n\t"
"popa");
printf("The result is %d\n", result);
return 0;
}
3.扩展asm格式
asm(“assembly code” : output location : input operands : changed registers);
asm(汇编程序 : 输出 : 输入 : 改变的寄存器)
扩展汇编指定寄存器会用到一张约束表,比如:
- a -> eax ax al //a表示eax寄存器
- b -> ebx
- c -> ecx
- d -> edx
- S -> esi
- D -> edi
- r -> 使用任何可用的通用寄存器,用于占位符
- m -> 使用变量的内存位置,指令若至少需要一个寄存器,那么也得使用寄存器配合
其他
输出修饰符:
+ -> 读写
= -> 只写
%
&
扩展asm的好处是可以使用局部变量,也可以使用占位符,寄存器使用2个百分号,例子:
int main()
{
int data1 = 10;
int data2 = 20;
int result;
asm("imull %%edx, %%ecx\n\t"
"movl %%ecx, %%eax"
: "=a"(result)
: "d"(data1), "c"(data2));
printf("The result is %d\n", result);
return 0;
}
占位符例子1:
asm("assembly code"
: "=r"(result)
: "r"(data1), "r"(data2));
汇编语句直接用数字访问,%0表示result,%1表示data1,%2表示data2
占位符例子2:
asm("assembly code"
: "=r"(result)
: "r"(data1), "0"(data2));
汇编语句直接用数字访问,data2前面的0表示和第0个,也就是result公用一个变量
占位符例子3:
asm("assembly code"
: [value2] "=r"(result)
: [value1] "r"(data1), "0"(data2));
汇编语句使用%[value1]访问
4.内联汇编宏函数
跟C语言一样,可以把asm定义成宏,方便使用。
九.结束
在深入学习调试的过程中,看不懂汇编语言会成为阻碍前进的绊脚石,于是我就开始阅读本书,目的很明确,只求看懂不求会写。所以整个篇幅比较偏向于记录流水账,将调试过程中经常会涉及到的点整理下来,对模糊的点编写程序进行推敲,学完汇编后,今后在调试过程中,可以起到事半功倍的效果。尽管本书是基于Linux GNU编译器的AT&T汇编语法,但是汇编语法大同小异。
本书为了讲解汇编程序,使用了很多gdb调试技巧,gdb又是另外一块很大的内容,很值得去学习和整理。
本文只列举了常用指令,汇编指令非常多,可以到这个页面查询。
【汇编指令速查】
作者:gzshun. 原创作品,转载请标明出处!
来源:http://blog.csdn.net/gzshun