《汇编语言程序设计》读书笔记
https://book.douban.com/subject/1446250/
作者: [Richard Blum](https://book.douban.com/search/Richard Blum)
出版社: 机械工业出版社
译者: 马朝晖
ISBN: 9787111175322
不同语法
汇编语言有多重语法格式,不同的汇编器使用不同的语法编写程序。
x86汇编的两种语法:intel语法和AT&T语法
x86汇编一直存在两种不同的语法:
- 在intel的官方文档中使用intel语法,Windows也使用intel语
- 而UNIX平台的汇编器一直使用AT&T语法
movI %edx,%eax #AT&T写法这条指令
如果用intel语法来写,就是mov eax,edx,
- 寄存器名不加%号,
- 源操作数和目标操作数的位置互换,
- 字长也不是用指令的后缀l表示而是用另外的方式表示。
详细可以参考Linux Assembly HOWTO(http://tldp.org/ HOWTO/Assembly-HOWTO/,后文简称[AssemblyHOWTO])。
处理器相关
主要组件
处理器的主要组件:
- 控制单元。intel的NetBurst技术包括的特性:
- 指令预取和解码
- 分支预测
- 乱序执行
- 退役
- 执行单元
- 标志
- 寄存器
寄存器
IA-32系列处理器的寄存器:
- 通用寄存器。8个32位寄存器,用于正在处理的数据。
- 段寄存器。6个16位寄存器,用于处理内存访问。
- 指令指针寄存器。单一的32位寄存器,指向要执行的下一条指令码。
- 浮点数据寄存器。8个80位寄存器,用于浮点数学数据。
- 控制寄存器。5个32位寄存器,用于确定处理器的操作模式。
- 调试寄存器。8个32位寄存器,用于在调试处理器时包含信息。
通用寄存器
E/ABCD/X;E/DS/I;E/SB/P
虽说都是通用寄存器,但是每个寄存器都有其不同的作用,在读完全部汇编语言的知识后才能真正理解。
- EAX。用于操作数和结果数据的累加器。
- EBX。指向数据内存段中的数据指针。
- ECX。字符串和循环数据的计数器。
- EDX。I/O指针。
- EDI。用于字符串操作的目标的数据指针。
- ESI。用于字符串操作的源的数据指针。
- ESP。堆栈指针。
- EBP。堆栈数据指针。
32位的E/ABCD/X寄存器也可以通过16位和8位的名称引用。以EAX为例:
- AL低8位
- AH 8-15位
- AX 低16位
段寄存器
6个16位。CDEFG/S,SS
CS DS SS 分别对应代码段的指针、数据段的指针、堆栈段的指针
ES FS GS 对应三个附加段指针
指令指针寄存器
指令指针寄存器即EIP寄存器,也叫PC寄存器。他跟踪要执行的下一条指令码。
控制寄存器
5个32位寄存器。CR/01234。
不能直接访问控制寄存器中的值,但是可以传送给通用寄存器。
标志/EFLAGS寄存器
单一的32位寄存器。 是可以用于确定程序的功能是否成功执行的唯一途径。比如减法,如果结果为负值,那么处理器内就有一个专门的标志被设置。
按照功能标志被分为三组:
- 状态标志。用于表明处理器进行数学运算的结果。比如:CF 进位标志,最高位是否产生了进位或者借位。SF 符号标志,有符号数的符号位。
- 控制标志。目前只有一个,DF,即方向标志,用于控制处理器处理字符串的方式。DF设置为1,字符串指令自动递减内存地址以便达到字符串的下一个字节。DF设置为0,字符串指令自动递增内存地址以便达到字符串的下一个字节。
- 系统标志。用于控制操作系统级别的操作,应用程序不应该试图修改系统标志。比如IF是中断使能标志,控制处理器如何响应从外部源接收到的信号。
条件传送指令使用的特定位在下表中介绍:
EFLAGS位 | 名称 | 描述 |
---|---|---|
CF | 进位标志(Carry) | 数学表达式产生了进位或者借位 0:没产生; 1:产生了 |
OF | 溢出标志(Overflow) | 整数值过大或者过小 |
PF | 奇偶校验(Parity) | 寄存器包含数学操作造成的错误数据 |
SF | 符号标志(sign) | 指出结果为正还是负 |
ZF | 零标志(zero) | 数学操作的结果为零 |
调试器中寄存器信息
在我本机我尝试看了下调试器中寄存器信息如下:
(gdb) info reg
rax 0x40057d 4195709
rbx 0x0 0
rcx 0x4005a0 4195744
rdx 0x7fffffffe248 140737488347720
rsi 0x7fffffffe238 140737488347704
rdi 0x1 1
rbp 0x7fffffffe150 0x7fffffffe150
rsp 0x7fffffffe150 0x7fffffffe150
r8 0x7ffff7dd5e80 140737351868032
r9 0x0 0
r10 0x7fffffffdca0 140737488346272
r11 0x7ffff7a2f460 140737348039776
r12 0x400490 4195472
r13 0x7fffffffe230 140737488347696
r14 0x0 0
r15 0x0 0
rip 0x400581 0x400581 <main+4>
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
64位的都是rxx。可以看到通用寄存器(包含堆栈指针等)、段寄存器、标志、指令指针寄存器等等。
IA-32高级特性FPU/MMX/SSE/SSE2/SSE3
- x87浮点单元(FPU)。引入了附加的寄存器,FPU寄存器。包括:数据寄存器,状态寄存器,控制寄存器,标志寄存器,FIP寄存器,FDP寄存器,操作码寄存器。
- 多媒体扩展,MMX。支持单指令多数据的执行模型,SIMD。支持MMX的处理可以处理64位打包的字节/字/双字整数。
- 流化SIMD扩展,SSE。支持128位打包的单精度浮点数。SSE2支持128位打包双精度浮点数,128位打包的字节/字/双字/四字整数。SSE3没有支持新的数据类型,但是引入了几个新的指令用于处理XMM寄存器中的整数和浮点数。
- 现在稍新一点的CPU还有rdtscp lm constant_tsc SSE4_1 SSE4_2等特性
指令码、指令前缀
指令码 instruction code
所有的计算机处理器都按照制造厂商在处理器芯片内部定义的二进制代码操作数据。这些预置的代码被称为指令码。
指令指针和数指针
为了区分数据和指令码,用指令指针和数据指针跟踪数据和指令码的存储位置。
IA-32指令码格式由四个主要部分构成:
- 可选指令前缀。可以包含1个到4个修改操作码行为的1字节前缀。指令前缀按组又分成四种,每次每个组中的只能选一个:
- 锁定前缀和重复前缀。锁定前缀表示指令将独占地使用共享内存区域,对多处理器和超线程系统非常重要。重复前缀表示重复的功能(常常用在字符串处理时)
- 段覆盖前缀和分支提示前缀。段覆盖前缀定义可以覆盖定义了的段寄存器值的指令。分支提示前缀尝试向处理器提供程序在条件跳转语句中最可能的路径线索。
- 操作数长度覆盖前缀。操作数长度覆盖前缀通知处理器,程序将在这个操作码之内,切换16位和32位的操作数长度。
- 地址长度覆盖前缀。跟操作数长度覆盖前缀类似,只是此处切换的是16位和32位的内存地址。
- 操作码(opcode)。 唯一必选部分。定义要执行的功能。长度在1-3字节之间。
- 可选的修饰符。一些操作码需要另外的修饰符来定义执行的过程中涉及到什么寄存器和什么内存位置。修饰符包含在三个单独的值中:
- 寻址方式说明符字节(ModR/M)。组成是3个字段:Mod(2位 6-7) reg/opcode(3位 3-5) r/m(3位 0-2)
- 比例-索引-基址字节(SIB)。组成是3个字段:比例(2位 6-7) 索引(3位 3-5) 基址(3位 0-2)
- 1、2、4个的地址位移字节
- 可选的数据元素。指令码的最后一部分是该功能使用的数据元素。
最后注意区分指令码
和操作码
。
定义数据
在汇编中定义数据有两种途径:使用内存位置和使用堆栈。
使用内存位置
testvalue:
.long 150
message:
.ascii "This is a test message"
pi:
.float 3.14159
从上面示例开可以看出,有三个部分组成
- 指向一个内存位置的标记。 可以理解成标签、变量名都行。
- 内存字节的数据类型。
- 默认值。
使用内存中定义的数据:
movl testvalue %ebx
addl $10 %ebx
movl %ebx testvalue
从上面示例中可以看出,可以从变量中取出值放到寄存器中,然后也可以将寄存器中的值搬到变量地址(即可以理解成对变量赋值)
使用堆栈
堆栈是特殊的内存区域,常用于函数之间传递数据。堆栈指针用于指向堆栈中下一个内存位置以便放入或者读取数据。
命令
数据类型
- .long(32位整数和int相同)
- .ascii(文本字符串)
- .float
- .asciz(以空字符结尾的文本字符串)
- .byte(字节)
- .double
- .int(32位整数和long相同)
- .octa(16字节整数)
- .quad(8字节整数)
- .short(16位整数)
- .single(单精度浮点数同float)
- .equ 可在数据段以定义静态数据符号。 .equ LINUX_SYS_CALL, 0x80 使用时 movl $LINUX_SYS_CALL, %eax
- .fill buffer:\n .fill 10000 改命令是汇编器自动创建10000个数据元素,默认每个字段为1字节。这种会被打包到应用程序中,可以通过观察应用程序的大小发现。
- .comm 在bss段,声明未初始化的数据的通用内存区域。 格式 : .comm symbol, length
- .lcomm 在bss段,声明未初始化的数据的本地通用内存区域,只定义要多少空间,不需要指明类型。本地的意思是为了不会从本地汇编代码之外进行数据访问。
附:
- .asciz 命令,不同于.ascii。.asciz命令会在声明的字符串末尾添加空字符,这种可以配合printf函数使用,详细的参见函数调用章节。
- .lcomm 命令用于定义缓冲区,声明在.bss段。
- 可以一行定义多个值,可以理解成数组:size: \n .long 100,150,200,250。 每个long 4个字节,通过size+8可以访问200
- bss和data中声明数据的一个区别是:bss段声明的数据不包含在可执行文件中。data段声明的数据必须包含在可执行程序中。
定义段
使用 .section命令声明段。详细的参见下面 程序组成章节。
.data .bss .text .radata
data段用于声明为程序存储数据元素的内存区域,在声明之后,这一段落不能扩展,在整个程序中保持静态。
bss段也是静态的内存段。这个段落中的缓冲区的内存区域是由0填充的。
文本段用于存储指令码。
.rodata,与.data段类似 但是这里定义的变量只能只读。
定义标签
.globl 也可以用于声明外部成名可以访问的程序标签。在文本段。
汇编程序必要的程序起始点就是用.globl声明,.globl _start 详细的参见 定义起始点 章节。
参见 x86 Assembly Language Reference Manual 文档的 Pseudo Operations 章节。比国内的材料讲的要全,但是没有示例,.section .text .long等都有阐述。
section data text globl _start
汇编程序中以.开头的名称,称为汇编指示(Assembler Directive)或伪操作(Pseudo-operation)。
汇编指示并不是指令的助记符,不会被翻译成机器指令,而是给汇编器一些特殊指示。
.section指示把代码划分成若干个段(Section) 比如.section .data 也可以不写,直接.data,参见hellostring.s
.data段保存程序的数据,是可读可写的,相当于C程序的全局变量。hello.s中没有定义数据,所以.data段是空的。
.text段保存代码,是只读和可执行的,后面那些指令都属于.text段**。
.gIobI告诉汇编器,_start这个符号要被链接器用到,所以要在目标文件的符号表中标记它是一个全局符号。
.align
ALIGN bound Bound 可取值有:1、2、4、8、16。当取值为 1 时,则下一个变量地址对齐于 1 字节边界(默认情况)。当取值为 2 时,则下一个变量对齐于偶数地址。当取值为 4 时,则下一个变量地址为 4 的倍数。当取值为 16 时,则下一个变量地址为 16 的倍数,即一个段落的边界。 简单地说:如果前面一个byte的变量地址到0x00404004h,接着用ALIGN 4 ,那么后面变量的地址就是00404008h了而不是00404005h。
为了满足对齐要求,汇编器会在变量前插入一个或多个空字节。为什么要对齐数据?因为,对于存储于偶地址和奇地址的数据来说,CPU 处理偶地址数据的速度要快得多。 参考
.equ 与 =
可以理解成定义一个常量,比如hello.s中改造成pseudo_op.s:
# pseudo_op.s
.section .data
.equ testval, 4
.section .text
.globl _start
_start:
movl $1, %eax
movl $testval, %ebx
int $0x80
有人说equ不能对一个变量重复赋值,我试了下可以(估计这个说说法不是linux上跑的汇编(Linux下的AT&T语法(即GNU as 汇编语法)))
然后,同样的复制语义支持testval = 4 这种写法,但是不支持 testval equ 4这种写法(linux上验证的)
.string
简单的理解成定义一个字符串。参见hellostring.s
工具相关
编译
GNU汇编器。该汇编器不在单独的包中发布,是在binutils中一起发布。binutils中包括了addr2line,ar,as,c++filt,gprof,ld,nlmconv,nm,objcopy,objdump,ranlib,readelf,size,strings,strip,windres。
可以通过确认binutils包是否存在来判断其是否安装。
rpm -qa|grep binu
# binutils-2.27-44.base.el7.x86_64
cat /etc/redhat-release
# CentOS Linux release 7.9.2009 (Core)
# 编译汇编
as -o test.o test.s
# 编译支持调试的目标文件
as -gstabs -o test.o test.s
简单c程序
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Hello World!\n");
exit(0);
}
编译c,c编程汇编
# 编译c -o 编译又链接, -c只编译不链接
gcc -o ctest ctest.c
./ctest
Hello World!
# -gstabs编译带有调试信息
gcc -gstabs -o ctest ctest.c
# 将c编译到汇编 不用-o指定目标文件时结果就是放在与c同名的.s文件中
gcc -S ctest.c
cat ctest.s
c编译到汇编结果
.file "ctest.c"
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
movl $0, %edi
call exit
.cfi_endproc
.LFE2:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits
反编译objdump/gdb的disassemble
反编译可以用objdump也可以在gdb调试中用disassemble
objdump -d ctest
Disassembly of section .text:
0000000000400490 <_start>:
400490: 31 ed xor %ebp,%ebp
400492: 49 89 d1 mov %rdx,%r9
400495: 5e pop %rsi
400496: 48 89 e2 mov %rsp,%rdx
400499: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
40049d: 50 push %rax
40049e: 54 push %rsp
40049f: 49 c7 c0 10 06 40 00 mov $0x400610,%r8
4004a6: 48 c7 c1 a0 05 40 00 mov $0x4005a0,%rcx
4004ad: 48 c7 c7 7d 05 40 00 mov $0x40057d,%rdi
4004b4: e8 a7 ff ff ff callq 400460 <__libc_start_main@plt>
4004b9: f4 hlt
4004ba: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
gdb调试中反编译
(gdb) disassemble
Dump of assembler code for function main:
0x000000000040057d <+0>: push %rbp
0x000000000040057e <+1>: mov %rsp,%rbp
=> 0x0000000000400581 <+4>: mov $0x400630,%edi
0x0000000000400586 <+9>: callq 0x400450 <puts@plt>
0x000000000040058b <+14>: mov $0x0,%edi
0x0000000000400590 <+19>: callq 0x400480 <exit@plt>
End of assembler dump.
连接与执行文件
连接使用ld
ld -o test test.o
ELF文件格式/readelf工具
ELF文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,它有以下三种不同的类型:
- 可重定位的目标文件(Relocatable,或者Object File)
- 可执行文件(Executable)
- 共享库(Shared Object,或者Shared Library)
readelf 工具可以读取elf文件信息。
调试
用gdb调试,基础的b r这些就不提及了,跟汇编相关的有以下这些,有些在前文中已经提及。
- disassemble可以调试c程序时反汇编
- info reg可以调试时查看寄存器信息
- print/x $ebx 查看单个寄存器的值。print/x 显示十六进制 print/d 显示十进制 print/t显示二进制
- x命令可以显示特定内存位置的值,类似print。x命令的格式有点复杂: x/nyz 。比如x/42cb &oputput 表示以字符形式(y:c)展示output对应内存地址(&output)的前42(n:42)个字节(z:b)
- n是要显示的字段数(可以理解为长度)
- y是输出格式
- c是字符
- d是十进制
- x是十六进制
- z是要显示的字段长度
- b用于字节
- 用于16位字 半字
- w用于32位字
- break 可以break 汇编的起始点,即 break _star
- 汇编调试需要加nop的问题 _start: 开始的第一条指令用nop 否则导致调试不能正常操作
break _start示例:
╰─$ as -gstabs -o cpuid.o cpuid.s
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/cpuid
╰─$ ld -o cpuid cpuid.o
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/cpuid
╰─$ ./cpuid
The processor Vendor ID is 'GenuineIntel'
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/cpuid
╰─$ gdb ./cpuid
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/simon/600.self/01.code/01.c_cpp/02.program_basic_study/cpuid/cpuid...done.
(gdb) b *_start
Breakpoint 1 at 0x4000b0: file cpuid.s, line 10.
(gdb) r
Starting program: /home/simon/600.self/01.code/01.c_cpp/02.program_basic_study/cpuid/./cpuid
Breakpoint 1, _start () at cpuid.s:10
10 movl $0, %eax
(gdb)
静态库与动态共享库
静态库
静态库是把多个目标文件打包在一起的一个文件。一般是.a扩展名。
连接静态库时,把其中声明的用到的函数拷贝一份到可执行文件中。
涉及处理静态库的工具是ar、nm:
- 显示库文件中包含的目标文件, ar t libchap14.a
- 显示更详细的信息,ar tv libchap14.a
- nm显示存档文件的索引, nm -s libchap14.a|more
- 创建静态库,ar r libchap14.a square.o cpuidinfo.o areafunc.o
- gcc使用时, gcc -o inttest tnttest.c libchap14.a
动态共享库
共享库加载操作系统的通用区域中,程序运行时需要时,操作系统自动地把函数代码加载到内存中,并允许程序访问他。
处理共享库的工具:
- 创建 gcc -shared -o libchap14.so.1 square.o cpuidinfo.o areafunc.o
- 编译时使用 gcc -o inttest -L. -lchap14 inttest.c
- 查看可执行文件依赖什么共享库 ldd inttest
动态共享库的路径问题
有两种方式告诉程序查找动态共享库位于什么位置
- LD_LIBRARY_PATH对应的环境变量。多个用冒号:分隔
- /etc/ld.so.conf 文件
修改完ld.so.conf 文件后,需要用法ldconfig命令刷新其cache。
程序组成
段
几个常用的段:
- 数据段。.data 声明带有初始值的数据元素,一般是汇编程序中的变量。
- 只读数据段。 .rodata,与.data段类似 但是这里定义的变量只能只读。
- bss段。.bss 声明使用0值初始化的元素,一般用作汇编程序中的缓冲区。
- 文本段。.text 必须有此段。这是声明指令代码的地方。
使用 .section命令声明段。
定义起始点
当汇编语言程序转换成可执行的文件时,连接器必须知道指令码中的起始点是什么。
_start标签用于表示程序应该从这条指令开始运行。声明方式用.globl命令(.globl 也可以用于声明外部成名可以访问的程序标签,比如外部汇编语言或者c程序使用的,便签可以粗略的理解成函数名),如下:
.globl _start
_start:
示例参见下面的代码模板。
找不到起始点则报错如下:
ld: warning: cannot find entry symbol _start; defaulting to 0000000000400078
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/no_start
╰─$ cat no_start.s
.section .text
movl $1, %eax
movl $4, %ebx
int $0x80
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/no_start
╰─$ as -o no_start.o no_start.s
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/no_start
╰─$ ld -o no_start no_start.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000400078
连接器可以用-e参数自定义起始点名称。
ld -e _cus_start -o cus_start cus_start.o
╰─$ cat cus_start.s
.section .text
.globl _cus_start
_cus_start:
movl $1, %eax
movl $4, %ebx
int $0x80
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/custom_start
╰─$ as -o cus_start.o cus_start.s
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/custom_start
╰─$ ld -e _cus_start -o cus_start cus_start.o
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/custom_start
╰─$ ./cus_start
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/custom_start
╰─$ echo $?
4
基本模板
hello.s代码,仅仅设置返回值为4。
# hello.s
#.section .data
# 有初始值的变量放在这里
#.section .bss
# 没有初始值的变量放在这里
.section .text
# 指令区域
.globl _start
# 程序起始点
_start:
movl $1, %eax
movl $4, %ebx
int $0x80
编译、链接、执行、查看返回值
as hello.s -o hello.o
ld hello.o -o hello
./hello
echo $?
# 4
传送数据
用mov传送数据。
有movl movw movb movs等,区别在于大小:
- l用于32位字长
- w用于16位字长 (配套ax bx等使用)
- b用于8位字长 (配套al bl等使用)
- s 用于处理字符串,在字符串章节再讨论
传送不同的数据:
- 传送立即数。 movl $0, %eax movl $80, %ebx
- 在寄存器间传送数据。 movl %eax, %ecx
- 在内存和寄存器间传送数据。 movl value, %ecx
- 从寄存器传递给内存。 movl %ecx, value
- 使用变址的内存地址。变址内存模式(indexed memory model)。base_address(offset_address, index, size) 计算地址的方式为:base_address+offset_address+index*size。 如果其中的值为0,就可以忽略他们,但是仍需要用逗号作为占位符。
- 使用寄存器间接寻址。比如:movl $values, %edi movl %ebx, (%edi)。 values前面加$表示取其地址,%edi外面加括号表示指令把ebx中的值传送给edi寄存器中包含的内存位置,不加括号表示指令将ebx中值传送到edi寄存器中。
- 使用寄存器间接相对地址寻址。比如:movl %edx, 4(%edi) movl %edx, -4(%edi)。
传送数据case:
.section .data
value:
.int 1
values:
.int 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60
.section .text
.globl _start
_start:
nop
movl value, %ecx
movl $100, %ecx
movl %exc, value
movl $1, %eax
movl $0, %ebx
movl $2, %edi
movl values(, %edi, 4), %eax
movl $values, %edi
movl %ebx, (%edi)
movl %edx, 4(%edi)
movl %edx, -4(%edi)
int $0x80
条件传送
cmovx source, destination
x表示触发传送操作的条件,条件取决于EFLAGS寄存器中的当前值。
EFLAGS寄存器涉及条件传送的标志位的说明请参见EFLAGS章节。
无符号条件传送
指令对 | 描述 | EFLAGS状态 |
---|---|---|
CMOVA/CMOVNBE | 大于/不小于等于 | CF或者ZF=0 |
CMOVAE/CMOVNB | 大于或者等于/不小于 | CF=0 |
CMOVNC | 无进位 | CF=0 |
CMOVB/CMOVNAE | 小于/不大于或者等于 | CF=1 |
CMOVC | 进位 | CF=1 |
CMOVBE/CMOVNA | 小于或者等于/不大于 | CF或者ZF=1 |
CMOVE/CMOVZ | 等于/零 | ZF=1 |
CMOVNE/CMOVNZ | 不等于/不为零 | ZF=0 |
CMOVP/CMOVPE | 奇偶校验/偶校验 | PF=1 |
CMOVNP/CMOVPO | 非奇偶校验/奇校验 | PF=0 |
A 大于 B小于 C进位 N不是 P 奇偶校验 PE偶校验 PO奇校验
*什么时候CF=0 cmp怎么比较
注意: A 是大于,即CF或者ZF=0。那么,什么时候CF或者ZF=0?
从下面的例子中可以看出:cmp %ebx, %eax时,当%eax>%ebx时,不借位,即CF=0。
cmp指令是拿第二个操作数减去第一个操作数,这个点一定要注意。
带符号的条件传送指令
指令对 | 描述 | EFLAGS状态 |
---|---|---|
CMOVGE/CMOVNL | 大于或者等于/不小于 | SF异或OF=0 |
CMOVL/CMOVNGE | 小于/不大于或者等于 | SF异或OF=1 |
CMOVLE/CMOVNG | 小于或者等于/不大于 | SF异或OF=1或者ZF=1 |
CMOVO | 溢出 | OF=1 |
CMOVNO | 未溢出 | OF=0 |
CMOVS | 带符号(负) | SF=1 |
CMOVNS | 无符号(非负) | SF=0 |
G 大于 L小于 E等于 N不是 O溢出 S 带符号
CMOV示例程序
查找最大值:
.section data
output:
.asciz "The larget value is %d\n"
values:
.int 105,235,61,315,134,221,53,145,117,5
.section text
.globl _start
`_start:
nop
movl values, %ebx
movl $1, %edi
loop:
movl values(, %edi, 4), %eax
cmp %ebx, %eax
cmova %eax, %ebx
inc %edi
cmp $10, %edi
jne loop
pushl %ebx
pushl $output
call printf
addl $8, %esp
pushl $0
call exit
cmp指令是拿第二个操作数减去第一个操作数,这个点一定要注意。当%eax>%ebx时,不借位,即CF=0。
交换指令 XCHG CMPXCHG CMPXCHG8B等
指令 | 说明 |
---|---|
XCHG | 在两个寄存器之间或者寄存器和内存之间就交换数据 |
BSWAP | 反转一个32位寄存器中的字节顺序 |
XADD | 交换两个值并把总和存储在目标操作数中 |
CMPXCHG | 把一个值和一个外部值进行比较,并且交换它和另一个值 |
CMPXCHG8B | 比较两个64位值并交换他们 |
XCHG 使用要点
- XCHG operand1 operand2, operand1 operand 不能同时是内存地址,两个操作数可以是8 16 32位,但是两个操作数长度需要相同。
- 当一个操作数是内存位置时,处理器的LOCK信号被自动标明,防止在交换过程中任何其他处理器访问这个位置。LOCK处理是非常耗时的。
冒泡排序case:
.section .data
values:
.int 105, 235, 61, 315, 134, 221, 53, 145, 117, 5
.section .text
.globl _start
_start:
movl $values, %esi
movl $9, %ecx
movl $9, %ebx
loop:
movl (%esi), %eax
cmp %eax, 4(%esi)
jge skip
xchg %eax, 4(%esi)
movl %eax, (%esi)
skip:
add $4, %esi
dec %ebx
jnz loop
dec %ecx
jz end
movl $values, %esi
movl %ecx, %ebx
jmp loop
end:
movl $1, %eax
movl $0, %ebx
int $0x80
CMPXCHG 使用要点
- cmpxchg指令比较目标操作数和EAX、AX、AL寄存器中的值,如果两个值相等,就把源操作数值加载到目标操作数中。如果不等,就把目标操作数值加载到EAX、AX、AL寄存器中。格式是:cmpxchg source, destination
- 如果是操作8字节的,请使用CMPXCHG8B
堆栈
相关指令
- 压栈:pushx source
- x 支持 l(32位) w(16位)
- source数据元素支持:
- 16位/32位寄存器
- 16位/32位内存值
- 16位段寄存器
- 8位/16位/32位立即数
- 出栈:popx destination
- destination接收的数据元素支持:
- 16位/32位寄存器
- 16位段寄存器
- 16位/32位内存地址
- destination接收的数据元素支持:
- 压入和弹出所有寄存器
- pusha和popa (16位) pushad和popad (32位)
- 压入寄存器顺序:DI SI BP BX DX CX AX
- pushf和popf EFLAGS的低16位 pushfd和popfd EFLAGS的32位
- push和pop不是把数据压入和弹出堆栈的唯一途径,我们也可以手动修改ESP寄存器作为内存指针,手工地把数据存放到内存中。通常做法是:很多程序是把ESP寄存器的值复制到EBP寄存器,而不是使用ESP寄存器本身。在汇编语言函数中经常使用EBP指针指向函数的工作堆栈空间基址。访问存储在堆栈中参数的指令相对于EBP值引用这些参数。
优化内存访问
内存访问是处理器执行最慢的功能之一,奔腾4缓存块长度是64位,如果定义的元素超过64位块的边界,就必须用两次缓存操作才能获取或者存储内存中的数据元素,为了解决这个问题,intel建议:
- 按照16字节边界对准16位数据
- 对准32位数据使它的基址是4的倍数
- 对准64位数据使它的基址是8的倍数
- 避免很多小的数据传输,而是用单一的大型数据传输
- 避免在堆栈中使用大的数据长度(比如80位和128位浮点值)
gas汇编器支持.align命令用于在特定的内存边界对准定义的数据元素。
控制流程
指令指针
指令指针是确定程序中的哪一条指令是应该执行的下一条指令。
指令长度可能是多个字节,所以指向下一条指令的不仅仅是每次使指令指针递增1。
程序不能直接使用mov指令修改EIP寄存器的值。但是分支指令可以改动EIP寄存器的值。
无条件分支
无条件分支有3种:
- 跳转
- jmp location
- location指要跳转的内存地址。
- 中断
- 中断分为软中断、硬中断。
- 使用带有0x80值的INT指令把控制转移给linux系统调用程序。
- 中断发生时,按照EAX寄存器的值执行子函数。如:在调用linux系统调用exit函数之前,把值1放到eax寄存器中。1是系统调用编号。 具体查看系统调用编号的办法参见《使用系统调》用这个部分。
- 调用
- call address address操作数引用程序中的标签,它被转换为函数中的第一条指令的内存地址。
- 调用完成后要跟返回指令 ret
条件分支
条件分支指令:
指令 | 说明 | EFLAGS状态位 |
---|---|---|
JA | Jump if above | CF=0 and ZF=0 |
JAE | Jump if above or equal | CF=0 |
JB | Jump if below | CF=1 |
JBE Jump | if below or equal | CF=1 or ZF=1 |
JC | Jump if carry | CF=1 |
JCXZ | Jump if CX register is 0 | |
JECXZ | Jump if ECX register is 0 | |
JE | Jump if equal | ZF=1 |
JG | Jump if greater | ZF=0 and SF=OF |
JGE | Jump if greater or equal | SF=OF |
JL | Jump if less | SF<>OF |
JLE | Jump if less or equal | ZF=1 or SF<>OF |
JNA | Jump if not above | CF=1 or ZF=1 |
JNAE | Jump if not above or equal | CF=1 |
JNB | Jump if not below | CF=0 |
JNBE | Jump if not below or equal | CF=0 and ZF=0 |
JNC | Jump if not carry | CF=0 |
JNE | Jump if not equal | ZF=0 |
JNG | Jump if not greater | ZF=1 or SF<>OF |
JNGE | Jump if not greater or equal | SF<>OF |
JNL | Jump if not less | SF=OF |
JNLE | Jump if not less or equal | ZF=0 and SF=OF |
JNO | Jump if not overflow | OF=0 |
JNP | Jump if not parity | PF=0 |
JNS | Jump if not sign | SF=0 |
JNZ | Jump if not zero | ZF=0 |
JO | Jump if overflow | OF=1 |
JP | Jump if parity | PF=1 |
JPE | Jump if parity even | PF=1 |
JPO | Jump if parity odd | PF=0 |
JS | Jump if sign | SF=1 |
JZ | Jump if zero | ZF=1 |
对于计算无符号整数值时使用带有above、below关键字的指令。
对于计算有符号整数值时使用带有grater、less关键字的指令。
比较
CMP指令
- CMP指令格式是: cmp operand1, operand2 CMP指令是把第二个操作数和第一个操作数比较,背后是operand2-operand1,进而设置EFLAGS寄存器。 再配合上面的条件分支完成跳转。
专门设置进位标志的指令
除了cmp可以设置进位标志,也有专门的指令可以设置进位标注:
指令 | 说明 |
---|---|
CLC | Clear the carry flag (set it to zero) |
CMC | Complement the carry flag (change it to the opposite of what is set) |
STC | Set the carry flag (set it to one) |
循环
循环指令:
指令 | 说明 |
---|---|
LOOP | Loop until the ECX register is zero |
LOOPE/LOOPZ | Loop until either the ECX register is zero, or the ZF flag is not set |
LOOPNE/LOOPNZ | Loop until either the ECX register is zero, or the ZF flag is set |
循环开始前,需要设置ECX寄存器的值,用于表示循环的次数,大致模板是:
mov $100, %ecx
loop1:
loop loop1
数字与数学运算
长度小的数传送给长度大的数
movzx source, destination 这条指令把长度小的无符号整数值(可以是寄存器,可以是内存值)传送给长度大的无符号整数值(智能是寄存器)。 source可以是8/16位,destination可以是16/32位。
movsx 与movzx类似,但是他解决的是传送有符号整数,比如-1,传送后要正确的为这个值设置高位为1(高位设置1表示负数)。
四字带符号整数
.int .short .long 命令定义的带符号整数是双字的,用.quad命令可以创建四字的带符号整数(每个值8个字节)。
MMX整数
- 一个MMX寄存器是64位的。 那么一个MMX寄存器里面就可以放多个整数(可以理解成一个数组),具体多少个,要看放的是字节整数,还是字整数,还是双字整数。详细的参见下面。
- MMX整数包括
- 64位打包字节整数 - 8个8位的字节整数
- 64位打包字整数 - 4个16位字整数
- 64位打包双字整数 - 2个32位双字整数。
- 传送MMX整数:movq 操作mmx寄存器、sse寄存器或者64位内存地址(但是不能在内存位置之间传送mmx整数)。
SSE整数
- 与MMX类似,只是SSE使用的是128位XMM寄存器(XMM与MMX很像,但是不同),因为长度扩大一倍,里面放置的数量也多一倍。比如可以放16个位的字节整数。
- 传送SSE整数:movdqa movdqu (对齐16个字节边界的数据用A选项,否则用U选项)
传送浮点值
- fld指令用于把浮点值传送如何传送出FPU寄存器。 格式是 fld source
IA-32指令集也提供一些预置浮点值,可以直接用指令把他们加载到FPU寄存器堆栈中。
指令 | 描述 |
---|---|
FLD1 | Push +1.0 into the FPU stack |
FLDL2T | Push log(base 2) 10 onto the FPU stack |
FLDL2E | Push log(base 2) e onto the FPU stack |
FLDPI | Push the value of pi onto the FPU stack |
FLDLG2 | Push log(base 10) 2 onto the FPU stack |
FLDLN2 | Push log(base e) 2 onto the FPU stack |
FLDZ | Push +0.0 onto the FPU stack |
长度小的打包数传送/转换给长度大的打包数
movaps/movups/movss/movlps/movhps/movlhps/movhlps/movapd/movupd/movsd/movhpd/movlpd
cvtdq2pd/cvtdq2ps/cvtpd2da/CVTPD2PI/CVTPD2PS/CVTPI2PD/CVTPI2PS/CVTPS2DQ/CVTPS2PD/CVTPS2PI/CVTTPD2PI/CVTTPD2DQ/CVTTPS2DQ/CVTTPS2PI
基本数学功能
简单加法(32位以内)
add source destination 不能同时使用内存地址作为源和目标操作数。 结果存放在目标位置。 addb addw addl 操作不同长度
adc source destination
大数加法(超过32位)
超过32位(且小于64位)的数字,要用两个32位的寄存器组合保存。 adc指令能将上一条add指令产生的进位自动加入这次运算,无需使用判断指令判断溢出寄存器后再操作。
adc示例程序:
.section .data
data1:
.quad 7252051615
data2:
.quad 5732348928
output:
.asciz "The result is %qd\n"
.section .text
.globl _start
_start:
movl data1, %ebx
movl data1+4, %eax
movl data2, %edx
movl data2+4, %ecx
addl %ebx, %edx
adcl %eax, %ecx
pushl %ecx
pushl %edx
push $output
call printf
addl $12, %esp
pushl $0
call exit
%qd 显示64位带符号整数值
adcl 紧接在addl后面 addl产生的进位被adcl透明处理
减法
sub 同add用法类似
sbb 同adc用法类似
递增和递减
dec destination
inc destination
两条指令可以用于递增和递减,注意这两条指令用于无符号整数。
乘法
mul source 无符号整数乘法
乘法指令的目标操作数约定放在EAX、AX、AL中,具体到哪个,目标操作数和结果目标位置有点复杂:
- 根据长度确定到哪个
- 由于乘法会产生很大的值,所以目标位置必须是源操作数的两倍长度。源值是8位,那么目标寄存器是AX。
- 但是当源值是16位时,目标寄存器却不是EAX,为了兼容,intel使用DX:AX寄存器对保存32位乘法结果值。
- 对于32位的源值,目标位置使用64位EDX:EAX寄存器对。
总结:
源操作数长度 | 目标操作数 | 结果目标位置 |
---|---|---|
8位 | AL | AX |
16位 | AX | DX:AX |
32位 | EAX | EDX:EAX(中文版有错,英文版本正确) |
示例程序:
.section .data
data1:
.int 315814
data2:
.int 165432
result:
.quad 0
output:
.asciz "The result is %qd\n"
.section .text
.globl _start
_start:
nop
movl data1, %eax
mull data2
movl %eax, result
movl %edx, result+4
pushl %edx
pushl %eax
pushl $output
call printf
add $12, %esp
pushl $0
call exit
mul用于无符号整数,imul用于带符号整数。
除法
div divisor
divisor是除数,被除数是隐含的。 执行指令之前需要被将被除数放置到指定寄存器。
不同长度的被除数放置的寄存器略有差别:
- 16位 AX寄存器
- 32位 DX:AX寄存器
- 64位 EDX:EAX寄存器
被除数的长度和除数的长度一一搭配,64位被除数搭配32位除数,32位被除数搭配16位除数,16位被除数搭配8位除数。
除法的结果是两个单独的数字:商和余数。这两个值被存储在和被除数值使用的相同的寄存器中。
被除数寄存器 | 被除数长度 | 商寄存器 | 余数寄存器 |
---|---|---|---|
AX | 16位 | AL | AH |
DX:AX | 32位 | AX | DX |
EDX:EAX | 64位 | EAX | EDX |
idiv指令用法同div,只是他用在带符号的除法上。
移位乘法 左移
sal 向左算术移位
shl 向左逻辑移位
两个指令用法相同
3种不同的使用格式:
sal destoination // dest向左移一位,相当于x2
sal %cl, destination // dest向左移cl寄存器中的值的位数
sal shifter, destination // dest向左移shifter值的位数
sal也是在其其结尾加上一个字符表示目标值的长度。比如sall
移位除法 右移
shr 用于无符号整数
sar 可以用于有符号整数
逻辑操作
and or xor not
and or xor 使用格式相同
and source, destination
not指令使用单一操作数,既是源值也是目标结果位置。
位测试
有时候我们需要测试某个寄存器的某一位是否被设置了1。比如检查EFLAGS寄存器。
test指令被设计用来做此用途,其背后是执行and操作但是与and不同的特点在于其不修改目标值。
测试完了之后配合jnz使用
test $0x00200000, %eax
jnz cpuid
使用FPU操作浮点数
FPU基本概念
FPU包括8个80位的数据寄存器和3个16位寄存器,称为控制(control)、状态(status)和标记(tag)寄存器。
FPU的8个寄存器称为R0~R7,但是程序编写时不这么使用,FPU操作标准寄存器是把他连在一起连成一个堆栈,而且这个堆栈不太同于内存堆栈,他是循环使用,即最后一个连着第一个。
堆栈顶部的寄存器是st0,其余的是st1~st7。
基本用法
.section .data
value1:
.int 40
value2:
.float 92.4405
value3:
.double 221.440321
.section bss
.lcomm int1, 4
.lcomm control, 2
.lcomm status, 2
.lcomm result, 2
.section text
.globl _start
_start:
nop
finit // 初始化FPU
fstcw control // 复制fpu的控制寄存器到内存
fstsw status // 复制fpu的状态寄存器到内存
filds value1 // filds把一个双字整数值加载到fp寄存器中
fists int1 // fists获取fpu堆栈顶部的值(value1)赋值给相应位置(int1)
flds value2 // 加载单精度浮点数值 float
fldl value3 // 加载双精度浮点数值 double
fst %st(4) // fst指令用于把st0寄存器的数据传送到另一个FPU寄存器(st4)
fxch %st(1) // fxch指令用于交换st0寄存器和另一个FPU寄存器(st1)
fstps result // fstp指令复制fpu寄存器ST0中值到内存位置,之后把值从FPU寄存器堆栈中弹出。
movl $1, %eax
movl $0, %ebx
int $0x80
基本浮点运算
fadd 浮点数加法
fdiv 浮点数除法
fdivr 反向浮点数除法 (反向是指目标值减去源值,并且结果存在目标操作数位置, 反向除法类似)
fmul 浮点数乘法
fsub 浮点数减法
fsubr 反向浮点数减法
fadd source // 内存中的source位置值与st0寄存器相加
fadd %st(x), %st(0) // stx中的值与st0值相加,结果存在st0
fadd %st(0), %st(x) // st0中的值与stx值相加,结果存在stx
faddp %st(0), %st(x) // st0中的值与stx值相加,结果存在stx,并且弹出st0
faddp // st0中的值与st1值相加,结果存在st1,并且弹出st0
进阶浮点计算
F2XM1 Computes 2 to the power of the value in ST0, minus 1
FABS Computes the absolute value of the value in ST0
FCHS Changes the sign of the value in ST0
FCOS Computes the cosine of the value in ST0
FPATAN Computes the partial arctangent of the value in ST0
FPREM Computes the partial remainders from dividing the value in ST0 by
the value in ST1
FPREM1 Computes the IEEE partial remainders from dividing the value in
ST0 by the value in ST1
FPTAN Computes the partial tangent of the value in ST0
FRNDINT Rounds the value in ST0 to the nearest integer
FSCALE Computes ST0 to the ST1st power
FSIN Computes the sine of the value in ST0
FSINCOS Computes both the sine and cosine of the value in ST0
FSQRT Computes the square root of the value in ST0
FYL2X Computes the value ST1 * log ST0 (base 2 log)
FYL2XP1 Computes the value ST1 * log (ST0 + 1) (base 2 log)
处理字符串
movs CLD STD LEA 移动字符并自增移动光标
movs 把字符串从内存的一个位置传送到另一个位置。其目标操作数和源操作数都是隐含的。
源操作数的是ESI寄存器,其中放置的是要移动的字符串的内存地址。(source )
目标操作数的是EDI寄存器,其中放置的是字符串要复制到的目的地内存地址。(destination)
而且每次执行完movs后,EDI、ESI都会自动递增(有时也会递减,递增还是递减取决于EFLAGS寄存器中DF标志是否被清零),这样多次执行movs指令时,后面的字符开可以接着被mov。
为了达成DF被设置正确的值,也就是不断mov的字符串来自哪个方向(不断递增就是往右,不断递减就是往左),。可以用以下命令达成:
- CLD指令用于清零DF标志
- STD用于设置DF标志
除此之外,还有个LEA指令,用于把变量的内存地址送给寄存器,跟mov略有区别:
movl $output, %edi
lea output, %edi // 少了一个$
REP 重复执行传送字符串
rep与movsb一起使用,重复的每次传送1字节字符到另一个位置,直至ECX寄存器中值为。
.section .data
value1:
.ascii "This is a test string.\n"
.section .bss
.lcomm output, 23
.section .text
.globl _start
_start:
nop
leal value1, %esi
leal output, %edi
movl $23, %ecx
cld
rep movsb
movl $1, %eax
movl $0, %ebx
int $0x80
但是rep 如果配合movsl使用,那么要先用movsl传送,剩下的零头还是要用movsb传送。如果全用movsl传送,零头部分会发生错误。
.section .data
string1:
.asciz "This is a test of the conversion program!\n"
legth:
.int 43
divisor:
.int 4
.section .bss
.lcomm buffer, 43
.section .text
.globl _start
_start:
nop
leal string1, %esi
leal buffer, %edi
movl length, %ecx
shrl $2, %ecx // 除以4
cld
rep movsl
movl length, %ecx
andl $3, ecx // 相当于对4取模 相当于取余数
rep movsb
movl $1, %eax
movl $0, %ebx
int $0x80
REP还有些类似指令:
REPE 等于时重复
REPNE 不等于时重复
LODS STOS 存储和加载字符串
LODS 用于把内存中的字符串值传送到EAX寄存器。
LODS指令使用ESI寄存器作为隐含源操作数。ESI必须要包含要操作的字符串所在的内存地址。
LODS指令按照加载的数据的数量递增或者递减ESI寄存器。
STOS SCAS都会利用EAX寄存器。
虽然能用REP指令配合LODS使用,但是实际意义不大,因为EAX只能放4个字节。
STOS 是将寄存器中的字符传送到内存,配合REP使用,可以用在用0填充一段内存等场景
CMPS 比较字符串
与前面的指令一样,隐含的源操作数是ESI,隐含的目标操作数是EDI。
CMPS会从源字符串减去目标字符串,并且适当地设置EFLAGS的标志位。
CMPS可以配合JE使用实现判断跳转。
CMPS也可以配合REP使用跨越多个字符比较。但是REP只关心ECX中的计数值。这个问题可以用REPE、REPNE、REPZ、REPNZ
比较规则是:
长度相同时,按ascii码表顺序(即字典顺序)。大写字母小于小写字母。
长度不同时:
按照长度短的为准进行比较。但是有个特例,就是短字符串与等于长字符串中相同长度的字符时,那么长字符串大。
SCAS扫描字符串
SCAS指令用于扫描字符串搜索一个或者多个字符。
SCAS的隐含的目标操作数是EDI寄存器。EDI寄存器中存放的是要扫描的目标字符串的内存地址。
SCAS的要扫描字符存放在EAX/AX/AL寄存器中。
SCAS同样后接长度字符形成:SCASB,SCASW,SCASL
SCAS与REPNE、REPE一起使用方便性就能体现出来了。注意:指令的语义是相反的,REPNE是指扫描到特定字符串时停止。
当找到字符串时,EDI寄存器包含紧跟在定位到的字符串后面的内存地址。这是因为REPNE指令指定SCAS指令时是递增EDI寄存器。ECX寄存器包含搜索字符距离字符串末尾的位置。为了得到距离字符串开头的位置,需要用这个值减去字符串的长度并且反转符号。
SCAS通过扫描字符串的结尾来确定字符串的长度。
使用函数
使用c库函数示例
改造cpuid程序用printf函数输出:
.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 $output, %edi
movl %ebx, 28(%edi)
movl %edx, 32(%edi)
movl %ecx, 36(%edi)
# 把参数传递给函数printf通过压栈完成, 压栈顺序与函数参数声明顺序相反
pushl $buffer
pushl $output
call printf
# addl指令用于清空为printf函数在栈中准备的参数
addl $8, %esp
# 给函数exit压栈参数
pushl $0
call exit
#
注意此处使用的是 .asciz 命令,不同于.ascii。.asciz命令会在声明的字符串末尾添加空字符。此处使用 .asciz是为了配合printf函数使用,printf函要求以空字符结尾的字符串作为输出字符串。
.lcomm 命令用于定义缓冲区,声明在.bss段。
把参数传递给函数printf通过pushl压栈完成, 压栈顺序与函数参数声明顺序相反
*在汇编语言中使用c库函数时,必须把c库文件连接到程序的目标代码。标准的c动态库位于libc.so.x文件中,包括printf和exit函数。为了连接标准的c动态库,使用连接器的-l参数。注意连接时库名称规则:libc.so.x对应的库名称就是c,其余的都是固定格式。 *
*ld -dynamic-linker /lib/ld-linux.so.2 -o cpuid2 -lc cpuid2.o 。 *
连接时不仅仅要指定依赖的库,还要指定在运行时加载动态库的程序,对于linux来说ld-linux.so.2便是。
也直接用gcc编译,这样不用指定动态库加载器和链接动态库,gcc自动连接正确的c库函数,但是要将_start标签改为main标签。gcc -o cpuid2 cpuid2.s
c库函数的文档在man页第三部分:
man 3 exit
汇编函数定义与访问
定义的基本模板:
.type area, @function
area:
xxx
ret
call area
.type指令定义函数。
函数的结束由RET指令定义。
call指令用来访问(调用)函数。
函数的放置:汇编函数的定义可以放在调用的前面,也可以放在后面。
函数参数传递
参数的传递有三种方式:
- 使用寄存器,比如eax,函数通过对eax的取值完成数据的获取,调用方通过给eax送值完成参数的传送。
- 使用全局数据,比如在.section .bss中定义了 .lcomm radius, 4 这样子函数中直接使用radius变量。
- 使用堆栈
堆栈与局部变量
堆栈的结构(含局部变量)
图1—–刚刚发起函数调用时刻,堆栈的整体布局是(注意此时用ESP相对寻址):
+---------------------+
| |
+---------------------+
| |
+---------------------+
| |
+---------------------+
| function argument3 | 12(%esp)
+---------------------+
| function argument2 | 8(%esp)
+---------------------+
| function argument1 | 4(%esp)
+---------------------+
| return adress | (%esp)
+---------------------+
图2—–执行到函数内部时(用了常用操作堆栈的模板代码,参见下面代码),堆栈的整体布局是(注意此时用EBP相对寻址):
+---------------------+
| |
+---------------------+
| |
+---------------------+
| |
+---------------------+
| function argument3 | 16(%ebp)
+---------------------+
| function argument2 | 12(%ebp)
+---------------------+
| function argument1 | 8(%ebp)
+---------------------+
| return adress | 4(%ebp)
+---------------------+
| old EBP value | (%ebp)
+---------------------+
| local varible1 | -4(%ebp)
+---------------------+
| local varible2 | -8(%ebp)
+---------------------+
| local varible3 |-12(%ebp)
+---------------------+
| |
+---------------------+
| |
+---------------------+
| |
+---------------------+
涉及几个要点:
- 堆栈指针存放在ESP。也称为,ESP寄存器用于指向内存中堆栈顶部。这个顶部是指调用方发起调用后的那个时刻的栈。此时栈顶对应的是返回地址。如果有pushl和popl操作,那么栈顶地址就会发生改变。即ESP寄存器中的值会发生改变。即,上面图1。
- 栈的分配方向从大往小方向分配。
- 为啥从大往小,参见 进程的内存空间结构 部分。
- 预留(申请)栈空间,就是类似subl $8, %esp, 是减去,因为从大往小方向分配嘛。 清空堆栈就是 addl。
- 通常到执行到函数内部时(用了常用操作堆栈的模板代码,参见下面代码),堆栈整体布局是 函数参数、返回地址、旧的EBP的值、函数局部变量。即,上面图2。
- 图2为什么切换EBP相对寻址,而不是图1的ESP。因为函数中如果有把数据压入堆栈,那么ESP值就会发生改变。所以用EBP存放了在函数发生调用那一刻ESP的值。同时还为了能找到存放ESP值之前的EBP的值(有点绕口),就把EBP的值压入堆栈。这样就形成了图2的布局,此时EBP对应的是ESP的值,ESP是栈顶,栈顶此时又是旧的EBP值(刚压入进去的)
- 调用前,将参数压栈的顺序与函数声明的参数顺序相反。
函数常用操作堆栈的代码模板
上面部分讲的图2的堆栈结构及其关联的要点,都是建立这个函数常用操作堆栈的代码模板上的,具体代码模板是:
function:
pushl %ebp
movl %esp, %ebp
...
movl %eb, %esp
popl %ebp
ret
独立汇编库函数文件
汇编定义的函数可以写在独立的源文件中,然后可以编译成独立的目标文件,但是在连接发起调用的目标文件时要带上被调用的函数对应的目标文件。至于怎么连接被调用的c库函数,前面 使用c库函数示例 有讲。
as -gstabs -o area.o area.s
as -gstabs -o functest4.o functest4s
ld -o functest4 functest.o area.o
Linux进程的内存空间结构与使用命令行参数
linux为系统要执行的程序在内存中创建一个区域。每个程序都被分配到相同的寻你内存地址。操作系统再完成虚拟内存到物理内存的映射。
虚拟内存的地址空间是从0x80480000开始到0xbfffffff结束。
靠近0x80480000小的这一端存放程序代码和数据(来自.bss和.data段)。
靠近0xbfffffff大的一端存放堆栈数据,从大往小分配,这样也解释了上个部分提到的堆栈为啥从大往小分配。
另外,这个堆栈不仅仅包含函数调用堆栈,还包含其他一些要素,下面阐述。
堆栈从大到小方向依次包括:
环境变量、命令行参数、指向环境变量的指针、0x00000000、指向命令行参数3的指针、指向命令行参数2的指针、指向命令行参数1的指针、程序名称、参数数目。
ESP指向参数数目这个条目。
调试时需要的要点:
- print $esp 查看看esp值 即查看栈顶地址
- x/20x 0xbffff950 显示从指定内存位置开始的20个字节,格式是十六进制
- x/s 0xbffffa36 用x命令和指针对峙查看字符串值
使用系统调用
简单示例
使用cpuid指令结合输出和退出两个系统调用做个例子:
.section .data
output:
.ascii "The processor Vendor ID is 'xxxxxxxxxxxx'\n"
#.section .bss
.section .text
.globl _start
_start:
movl $0, %eax
cpuid
movl $output, %edi
movl %ebx, 28(%edi)
movl %edx, 32(%edi)
movl %ecx, 36(%edi)
# 开始向控制台输出的系统调用 strace看到的是write(6291690, NULL, 42)
# 系统调用编号
movl $4, %eax
# 要写入的文件描述符 STDOUT 标准输出的文件描述符是1
movl $1, %ebx
# 字符的开头
movl $output, %ecx
# 字符串的长度
movl $42, %edx
int $0x80
# 到上面结束向控制台输出的系统调用
movl $1, %eax
movl $0, %ebx
int $0x80
查看系统调用编号
查看系统调用编号
cat /usr/include/asm/unistd_32.h
#define __NR_write 4
注意32位与64位不同
编译与执行:
╰─$ as -o cpuid.o cpuid.s
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/cpuid
╰─$ ld -o cpuid cpuid.o
╭─simon@simon-c7-d1 ~/600.self/01.code/01.c_cpp/02.program_basic_study/cpuid
╰─$ ./cpuid
The processor Vendor ID is 'GenuineIntel'
寄存器的使用约定
几个要点:
- eax存放 要调用的哪个系统调用的编号
- ebx\ecx\edx\esi\edi 5个寄存器依次存放第1~5个参数
- 需要超过6个参数的系统调用,则我们需要在ebx中存放指向输入参数的内分位置的指针,输入参数按照连续的顺序存
- 参数传送顺序与系统调用声明参数的顺序相同
- 系统调用的返回值存放在eax寄存器
- 复杂的系统调用的返回值,需要在汇编中创建与之对应的结构接受,参见P276 sysinfo系统调用
strace
-p pid 附加到现有进程
-o file 将结果写入文件
-c 可以统计时间和次数等
-e 指定输出的过滤表达式
通过strace也能发现用c库函数比在汇编中直接调用系统调用,要多出很多系统调用(加载动态加载器程序等),但话说回来,这个开销并不明显。
使用内联汇编
内联汇编关键字、防止编译器优化与扩展格式
关键字分两组,不同标准使用不同的:
- asm与volatile
- __asm__ 与__volatile__
- volatile与__volatile__ 表示告知编译器不要优化此代码
基本格式
asm(“assembly code”)
多行的用自己用\n 显式表示, 也可以用多个引号。
示例:
#include <stdio.h>
int a = 10;
int b = 20;
int result;
int main()
{
asm ( “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 answer is %d\n”, result);
return 0;
}
此时这种格式访问的ab变量都得是全局变量,不能是全局变量。后面扩展格式可以访问局部变量。
扩展格式
asm (“assembly code” : output location : input operands : changed registers)
用三个冒号连接了四个部分,具体释义:
- 具体的汇编代码,用引号引起来
- 输出位置:包含内联汇编代码的输出值的寄存器和内存位置的列表
- 输入操作数:包含内联汇编代码输入值的寄存器和内存位置的列表
- 改动的寄存器:内联汇编代码改变的任何其他寄存器的列表 // 任何其他是指:内联代码中用到作为输入输出的就不用写在这,如果写了就会报错。所以很多时候,我们看到这部分没有。但是如果是用到的一些中间状态暂存的寄存器要写在这。详情参见P303
对于第二和第三部分的输出位置和输入操作数的格式是:
“constraint” (variable)
约束是单一字符,但是对于输出值,这个单一字符前面还可以加个修饰符,也就是可以是两个字符。
约束字符有 abcdSDrqAftumoVing
修饰字符有+=%&
约束与修饰符的具体含义参见P297 P298
a Use the %eax, %ax, or %al registers.
b Use the %ebx, %bx, or %bl registers.
c Use the %ecx, %cx, or %cl registers.
d Use the %edx, %dx, or $dl registers.
S Use the %esi or %si registers.
D Use the %edi or %di registers.
r Use any available general-purpose register.
q Use either the %eax, %ebx, %ecx, or %edx register.
A Use the %eax and the %edx registers for a 64-bit value.
f Use a floating-point register.
t Use the first (top) floating-point register.
u Use the second floating-point register.
m Use the variable’s memory location.
o Use an offset memory location.
V Use only a direct memory location.
i Use an immediate integer value.
n Use an immediate integer value with a known value.
g Use any register or memory location available.
+ The operand can be both read from and written to.
= The operand can only be written to.
% The operand can be switched with the next operand if necessary.
& The operand can be deleted and reused before the inline functions complete.
具体示例参见下面使用变量章节。
使用寄存器、c的变量、占位符
使用寄存器
基本格式访问的变量都得是全局变量,不能是全局变量,参见上面基本格式示例。
扩展格式可以访问局部变量:
#include <stdio.h>
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;
}
在扩展格式中使用寄存器必须是两个%。
使用、引用与替换占位符
使用示例:
asm (“assembly code”
: "=r"(result)
: "r"(data1), "r"(data2));
==>
#include <stdio.h>
int main()
{
int data1 = 10;
int data2 = 20;
int result;
asm ("imull %1, %2\n\t"
"movl %2, %0"
: "=r"(result)
: "r"(data1), "r"(data2));
printf("The result is %d\n", result);
return 0;
}
==>
%0 will represent the register containing the result variable value.
%1 will represent the register containing the data1 variable value.
%2 will represent the register containing the data2 variable value.
0 1 2 依照 result data1 data2顺序来的,然后在汇编代码中使用他。编译器自己选择寄存器。
%0 %1 %2这样的占位符不直观,可以使用占位符名称解决这个问题,如下:
asm ("imull %[value1], %[value2]"
: [value2] "=r"(data2)
: [value1] "r"(data1), "0"(data2));
==>
int main()
{
int data1 = 10;
int data2 = 20;
asm ("imull %[value1], %[value2]"
: [value2] "=r"(data2)
: [value1] "r"(data1), "0"(data2));
printf("The result is %d\n", data2);
return 0;
}
参考材料
汇编语言程序设计(美)布鲁姆 着,马朝晖 等….pdf
Professional Assembly Language.2005.pdf
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!