【读书笔记】汇编语言程序设计

时间:2022-12-24 04:51:53

【读书笔记】汇编语言程序设计

零.阅读目的

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 section, offset 长调用和跳转使用不同语法定义段和偏移值
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 //该指令经常用来将内存地址赋值给dest

  • DF标志
    每次执行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