当前位置 博文首页 > 木多:【原创】X86_64/X86 GNU汇编、寄存器、内嵌汇编
整理的X86_64/X86汇编、寄存器、C内嵌汇编笔记,主要用于查阅使用。
计算机的处理器有很多不同的架构,比如 x86-64、ARM、Power 等,每种处理器的指令集都不相同,那也就意味着汇编语言不同。目前的电脑,CPU 一般是 x86-64 架构,是 64 位机。
C语言代码:
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("Hello %s!\n", "WSG");
return 0;
}
编译为汇编:
gcc -S -O2 hello.c -o hello.s
或
clang -S -O2 hello.c -o hello.s
对应的汇编代码如下:
.file "hello.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "WSG"
.LC1:
.string "Hello %s!\n"
.section .text.unlikely,"ax",@progbits
.LCOLDB2:
.section .text.startup,"ax",@progbits
.LHOTB2:
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB23:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $.LC0, %edx
movl $.LC1, %esi
movl $1, %edi
xorl %eax, %eax
call __printf_chk
xorl %eax, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE23:
.size main, .-main
.section .text.unlikely
.LCOLDE2:
.section .text.startup
.LHOTE2:
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
汇编语言的组成元素:指令、伪指令、标签和注释,每种元素独占一行
指令:
助记符 操作数(源,目的)
伪指令以"."开头,末尾没有冒号":"。伪指令是是辅助性的,汇编器在生成目标文件时会用到这些信息,但伪指令不是真正的 CPU 指令,就是写给汇编器的。每种汇编器的伪指令也不同,要查阅相应的手册。常见的汇编器伪指令如下。
.file "hello.c"
.section .rodata.str1.1,"aMS",@progbits,1
标签以冒号“:”结尾,用于对伪指令生成的数据或指令做标记。标签很有用,它可以代表一段代码或者常量的地址(也就是在代码区或静态数据区中的位置)。可一开始,我们没法知道这个地址的具体值,必须生成目标文件后,才能算出来。所以,标签会简化汇编代码的编写。
.LC1:
.string "Hello %s!\n"
注释以“#”号开头,与C语言中//表示注释是一样的。
在代码中,助记符movq
,xorl
中的mov
和xor
是指令,而q
和l
叫做后缀,表示操作数的位数。后缀一共有 b, w, l, q 四种,分别代表 8 位、16 位、32 位和 64 位。
比如,movq
中的 q 代表操作数是 8 个字节,也就是 64 位的。movq
就是把 8 字节从一个地方拷贝到另一个地方,而 movl
则是拷贝 4 个字节。
而在指令中使用操作数,可以使用四种格式,它们分别是:立即数、寄存器、直接内存访问和间接内存访问。
操作数可以表示立即数(常数)值、寄存器值或是来自内存的值。比例因子\(s\)必须是1、2、4或者8.
立即数以 $ 开头, 比如 $40。(下面这行代码是把 40 这个数字拷贝到 %eax 寄存器)。
movl $40, %eax
除此之外,在指令中最常见到的就是对寄存器的访问,GNU 的汇编器规定寄存器一定要以 % 开头。
直接内存访问:当我们在代码中看到操作数是一个数字时,它其实指的是内存地址。不要误以为它是一个数字,因为数字立即数必须以 $ 开头。另外,汇编代码里的标签,也会被翻译成直接内存访问的地址。比如callq _printf
中的_printf
是一个函数入口的地址。汇编器帮我们计算出程序装载在内存时,每个字面量和过程的地址。
间接内存访问:带有括号,比如(%rbp),它是指 %rbp 寄存器的值所指向的地址。
间接内存访问的完整形式是:
偏移量(基址,索引值,字节数)这样的格式。
其地址是:
基址 + 索引值 * 字节数 + 偏移量
举例来说:
8(%rbp),是比 %rbp 寄存器的值加 8。
-8(%rbp),是比 %rbp 寄存器的值减 8。
(%rbp, %eax, 4)的值,等于 %rbp + %eax*4。这个地址格式相当于访问 C 语言中的数组中的元素,数组元素是 32 位的整数,其索引值是 %eax,而数组的起始位置是 %rbp。其中字节数只能取 1,2,4,8 四个值。
几个常用的指令:
mov
mov 寄存器|内存|立即数, 寄存器|内存
这个指令最常用到,用于在寄存器或内存之间传递数据,或者把立即数加载到内存或寄存器。mov 指令的第一个参数是源,可以是寄存器、内存或立即数。第二个参数是目的地,可以是寄存器或内存。
lea:lea 是“load effective address”的意思,装载有效地址,实际是mov指令的变形。其操作不影响任何条件码
lea 源,目的
参数为标准格式中给定的内存位置,但并不加载内存位置的内容,而是加载计算得出的地址。例如:如果寄存器%rdx的值为x,那么指令leaq 7(%rdx,%rdx,4),%eax
将设置寄存器%rax
的值为5x+7
。
cld
该指令清除了标志寄存器中的DF位。 清除方向标志后,所有字符串操作(如stos,scas和其他操作)都会使索引寄存器esi或edi递增。
std
与cld相反,该指令置位了标志寄存器中的DF位。 置位方向标志后,所有字符串操作(如stos,scas和其他操作)都会使索引寄存器esi或edi递减。
stosl
stosl指令将eax复制到es:di中,若设置了EFLAGS中的方向位置位(即在STOSL指令前使用STD
指令)则EDI自减4,否则(使用CLD
指令)EDI自增4;
rep
重复执行%ecx次,如rep; stosl
表示重复执行stosl
,直到cx为0,例:
cld;rep;stosl
cld设置edi或同esi为递增方向,rep做(%ecx)次重复操作,stosl表示edi每次增加4。
指令 | 描述 |
---|---|
push 源 | 把源压入栈 |
pop 目的 | 把栈顶的元素放入目的 |
pushl %eax
相当于:
subl $4, %esp
mvol %eax,(%esp)
pushfl #表示将%eflage寄存器当前的数据入栈
popl %eax
相当于:
movl (%esp), %eax
addl $4, %esp
指令 | 描述 |
---|---|
sub 源, 目的 | 把目的中值减去源的值 |
imul 源, 目的 | 把目的乘上源 |
clto | 转换为8字(%rax符号扩展 →%rdx:%rax) |
xor 源, 目的 | 做异或运算 |
or 源, 目的 | 或运算 |
and 源, 目的 | 与运算 |
inc 目的 | 加一 |
dec 目的 | 减一 |
neg 目的 | 取负值 |
not 目的 | 按位取反 |
add 指令是做加法运算,它可以采取下面的格式:
add 立即数, 寄存器
add 寄存器, 寄存器
add 内存, 寄存器
add 立即数, 内存
add 寄存器, 内存
比如,典型的 c=a+b 这样一个算术运算可能是这样的:
movl -4(%rbp), %eax #把%rbp-4的值拷贝到%eax
addl -8(%rbp), %eax #把%rbp-8地址的值加到%eax上
movl %eax, -12(%rbp) #把%eax的值写到内存地址%rbp-12
and 对两个操作数的内容进行逻辑与运算,并将结果存储到第二个操作数,将溢出标志位及进位标志设置为FALSE。
not 对操作数的每一位逻辑取反,也称为一个数的补数
or 对两个操作数进行逻辑或,并将结果存储到第二个操作数,将溢出标志位设置为FLASE
adc 带进位加法。将进位位与第一个操作数与第二个操作数相加,如果存在溢出,就将溢出及进位标志设置为真。
cdq将%eax中的字带符号扩展为%eax:%eax组成的双字。q表示这是一个双字(64字节).这条指令通常在发出idivl指令之前。
cmp 比较两个整数,将第二个操作数减去第一个操作数,舍弃结果,设置标志位。
dec将寄存器或内存位置的数据减一。
div执行无符号除法。将%edx:%eax所含的双字除以指定寄存器或内存位置的值。运算后%eax包含商,%edx包含余数,如果商对于%eax来说过大,导致溢出,将触发中断0.
idiv执行有符号除法。
imul执行有符号乘法,将结果保存到第二个操作数。如果第二个操作数空缺,就默认为%eax,且完好的结果将存在%eax:%eax中
inc递增给定寄存器或地址。
mul执行无符号乘法,运算规则与imull相同
neg将给定寄存器或内存位置的内容补齐(二进制求补)
sbb错位减法,与adc用法相同。通常使用sub
sub将两个操作数相减,用第二个操作数减去第一个操作数,将结果保存的到第二个操作数,本指令可用于有符号整数及无符号整数
rcl将第一个操作数,向左循环移位给定次数,第一个操作数可以是立即数或寄存器%cl。循环移位包含进位标志,因此此指令实际上对33位而非32位进行操作。本指令将设置溢出标志
rcr向右循环移位,其他与上一条指令相同
rol向左循环移位,本指令设置溢出标志和进位标志,但不会将进位位作为循环移位的一部分。向左循环移位的次数可以通过立即寻址方式或寄存器%cl的值指定
ror向右循环移位,其他与上一条指令相同
sal算术左移,符号位移出至进位标志,最低有效位填充0,其他位左移。与一般左移相同,移动位数通过立即寻址方式或是寄存器%cl指定。
sar算术右移(填上符号位),最低有效位移出至进位标志,符号位被向右移入,并保留原符号位。其他位只是向右移。移动位数通过立即寻址方式或是寄存器%cl指定。
shl逻辑左移,将所有位左移(对符号位不做特殊处理).将最左一位推入进位标志,移动位数通过立即寻址方式或是寄存器%cl指定。
shr逻辑右移,将所有位右移(对符号位不做特殊处理).将最右一位推入进位标志,移动位数通过立即寻址方式或是寄存器%cl指定。
指令 | 描述 |
---|---|
cmp 源1, 源2 | 根据源1-源2设置状态码 |
test 源1, 源2 | 根据源1& 源2设置状态码 |
OF: 溢出标志.最近的操作导致一个补码溢出---正溢出或负溢出。
SF : 符号标志.最近的操作得到的结果为负数。
ZF:零标志,最近的操作得出的结果为0。
A 辅助进位标志。
P 奇偶标志,如果最后一个结果的低字节由偶数个1,此标志为真。
CF 进位标志,最近的操作使最高位产生了进位。可用来检查无符号操作的溢出。
指令 | 描述 |
---|---|
cli、sti | 清除IF标志位(CLear Interrupt flag)、置位IF标志(SeT Interrupt flag) |
pushfq、popfq | 将RFLAGS的值压栈和出栈 |
例如,用一条ADD指令完成等价于t=a+b的功能,这里a、b和t都是整型。然后根据结果来设置条件码:
CF
(unsigned) t < (unsigned) a
无符号溢出ZF
(t = 0)
零SF
(t < 0)
负数OF
(a < 0==b < 0) && (t < 0 !=a < 0)
有符号溢出
指令 | 描述 |
---|---|
jmp 标签或地址 | 跳转到某个位置的代码 |
call 标签或地址 | 把返回地址压入栈,并跳转到指定位置的代码 |
ret | 从栈里弹出返回地址,并跳转过去 |
call 将%eip所指的下一个值入栈,并跳转到目的地址。这用于函数调用。目的地址也可以是星号后跟寄存器的形式,这种方式为间接函数调用。例如 call *%eax
将调用%eax中所含地址所指的函数
int 引起给定数字的中断。
jxx条件分支。xx为条件(由前一条指令设置)为TRUE,就跳转到给定的地址;否则,执行下一条指令。条件代码如下:
指令 | 含义 | 状态码 |
---|---|---|
je或jz | 跳转,如果相等(等于零) | ZF |
jne或jnz | 跳转,如果不相等(等于零) | ~ZF |
js | 跳转,如果为负值 | SF |
jns | 跳转,如果不为负值 | ~SF |
jg或jnle | 跳转,如果大于,有符号数 | ~(SF^OF) & ~ZF |
jge或jnl | 跳转,如果大于等于,有符号数 | ~(SF^OF) |
jl或jnge | 跳转,如果小于,有符号数 | SF^OF |
jle或jne | 跳转,如果小于等于,有符号数 | SF^OF | ZF |
…… |
jmp无条件跳转。仅仅是将%eip设置为目的地址,目的地址也可以是星号后跟寄存器的形式,这种方式为间接函数调用。jmp *%eax
用寄存器中的值作为跳转目标,jmp *(%rax)
以内存地址%rax中的值作为跳转目标。
ret从栈种弹出值,并将%eip设置为该值,用于从函数调用返回。
.equ
允许你为数字分配名称。例如
.equ LINUX_SYSCALL,0x80
此时LINUX_SYSCALL
就是一个常量,使用如下
int $LINUX_SYSCALL
计算一段数据的长度
.section .data
helloworld:
.ascii "hello world\n"
helloworld_end;
.equ helloworld_len, helloworld_end - helloworld
.rept
用于填充每一项,.rept
告诉汇编程序将.rept
和.endr
之间的断重复指定次数.
.rept 30 #填充30字节0
.byte 0
.endr
结束以.rept定义的重复节(section)
.locmm
指令将创建一个符号,代指一个存储位置。使用.lcomm
创建一个符号my_buffer,代指.bss
段中用作缓冲区的500字节存储位置。
.section .bss
.locmm my_buffer, 500
movl $my_buffer, %ecx #将缓冲区地址加载到%ecx中
.globl
声明一个全局符号。
.globl _start
_start:
.type
指定一个符号作为某种类型。例如告诉链接器 符号power作为函数处理:
.type power, @function
power:
……
如果其他程序中没有使用该函数,则这条指令可以不需要。power:
将下一条指令的存储位置赋给符号power,这就是为什么调用该函数时需要如下执行:
call power
将给定带引号字符串转换为字节数据
将逗号分隔符的值列表作为数据插入程序
切换正在使用的节。通用节包括.text、.data、.bss
定义一个long型变量begin
如下:
begin:
.long 0
x86-64 架构的 CPU 里有很多寄存器,我们在代码里最常用的是 16 个 64 位的通用寄存器,分别是:
%rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp, %r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。
这些寄存器在历史上有各自的用途,比如,rax 中的“a”,是 Accumulator(累加器) 的意思,这个寄存器是累加寄存器。
但随着技术的发展,这些寄存器基本上都成为了通用的寄存器,不限于某种特定的用途。但是,为了方便软件的编写,我们还是做了一些约定,给这些寄存器划分了用途。针对 x86-64 架构有多个调用约定(Calling Convention),包括微软的 x64 调用约定(Windows 系统)、System V AMD64 ABI(Unix 和 Linux 系统)等,下面的内容属于后者:
%rax 除了其他用途外,通常在函数返回的时候,把返回值放在这里。
%rsp 作为栈指针寄存器,指向栈顶。
%rdi,%rsi,%rdx,%rcx,%r8,%r9 给函数传整型参数,依次对应第 1 参数到第 6 参数。超过 6 个参数使用。
如果程序要使用 %rbx,%rbp,%r12,%r13,%r14,%r15 这几个寄存器,是由被调用者(Callee)负责保护的,也就是写到栈里,在返回的时候要恢复这些寄存器中原来的内容。其他寄存器的内容,则是由调用者(Caller)负责保护,如果不想这些寄存器中的内容被破坏,那么要自己保护起来。
上面这些寄存器的名字都是 64 位的名字,对于每个寄存器,我们还可以只使用它的一部分,并且另起一个名字。比如对于 %rax,如果使用它的前 32 位,就叫做 %eax,前 16 位叫 %ax,前 8 位(0 到 7 位)叫 %al,8 到 15 位叫 %ah。
原本含义 | 64位 | 32位 | 16位 | 高8位 | 低8位 | |
---|---|---|---|---|---|---|
Accumulator | 累加器 | rax | eax | ax | ah | al |
Base | 基地址 | rbx | ebx | bx | bh | bl |
Counter | 计数器 | rcx | ecx | cx | ch | cl |
Data | 数据 | rdx | edx | dx | dh | dl |
Source | 源 | rsi | esi | si | sil | |
Destination | 目的 | rdi | edi | di | dil | |
Stack Base Pointer | 栈基址 | rbp | ebp | bp | bpl | |
Stack Pointer | 栈指针 | rsp | esp | sp | spi | |
后增加的8个通用寄存器 | r8-r15 | r8d-r15d | r8w-r15w | r8b-r15b |
除了通用寄存器以外,有可能的话,还要了解下面的寄存器和它们的用途,我们写汇编代码时经常跟它们发生关联:
8 个 80 位的 x87 寄存器,用于做浮点计算;
8 个 64 位的 MMX 寄存器,用于 MMX 指令(即多媒体指令),这 8 个跟 x87 寄存器在物理上是相同的寄存器。在传递浮点数参数的时候,要用 mmx 寄存器。
16 个 128 位的 SSE 寄存器,用于 SSE 指令。 (SIMD )。
指令寄存器,rip,保存指令地址。CPU 总是根据这个寄存器来读取指令。
flags(64 位:rflags, 32 位:eflags)寄存器:每个位用来标识一个状态。比如,它们会用于比较和跳转的指令,比如 if 语句翻译成的汇编代码,就会用它们来保存 if 条件的计算结果。
在 X86-64 架构下,有很多的寄存器,所以程序调用约定中规定尽量通过寄存器来传递参数,而且,只要参数不超过 6 个,都可以通过寄存器来传参,使用的寄存器如下:
32位名称 | 64位名称 | 所传参数 |
---|---|---|
%eax | %rax | 参数1 |
%esi | %esi | 参数2 |
%edx | %rdx | 参数3 |
%ecx | %rcx | 参数4 |
%r8d | %r8 | 参数5 |
%r9d | %r9 | 参数6 |
超过 6 个的参数的话,要再加上栈来传参:
根据程序调用约定的规定,参数 1~6 是放在寄存器里的,参数 7 和 8 是放到栈里的,函数参数以逆序的方向入栈,先放参数 8,再放参数 7。
int fun1(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8){
int c = 10;
return x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + c;
}
println("fun1:" + fun1(1,2,3,4,5,6,7,8));
# function-call2-craft.s 函数调用和参数传递
# 文本段,纯代码
.section __TEXT,__text,regular,pure_instructions
_fun1:
# 函数调用的序曲,设置栈指针
pushq %rbp # 把调用者的栈帧底部地址保存起来
movq %rsp, %rbp # 把调用者的栈帧顶部地址,设置为本栈帧的底部
movl $10, -4(%rbp) # 变量c赋值为10,也可以写成 movl $10, (%rsp)
# 做加法
movl %edi, %eax # 第一个参数放进%eax
addl %esi, %eax # 加参数2
addl %edx, %eax # 加参数3
addl %ecx, %eax # 加参数4
addl %r8d, %eax # 加参数5
addl %r9d, %eax # 加参数6
addl 16(%rbp), %eax # 加参数7
addl 24(%rbp), %eax # 加参数8
addl -4(%rbp), %eax # 加上c的值
# 函数调用的尾声,恢复栈指针为原来的值
popq %rbp # 恢复调用者栈帧的底部数值
retq # 返回
.globl _main # .global伪指令让_main函数外部可见
_main: ## @main
# 函数调用的序曲,设置栈指针
pushq %rbp # 把调用者的栈帧底部地址保存起来
movq %rsp, %rbp # 把调用者的栈帧顶部地址,设置为本栈帧的底部
subq $16, %rsp # 这里是为了让栈帧16字节对齐,实际使用可以更少
# 设置参数
movl $1, %edi # 参数1
movl $2, %esi # 参数2
movl $3, %edx # 参数3
movl $4, %ecx # 参数4
movl $5, %r8d # 参数5
movl $6, %r9d # 参数6
movl $7, (%rsp) # 参数7
movl $8, 8(%rsp) # 参数8
callq _fun1 # 调用函数
# 为pritf设置参数
leaq L_.str(%rip), %rdi # 第一个参数是字符串的地址
movl %eax, %esi # 第二个参数是前一个参数的返回值
callq _printf # 调用函数
# 设置返回值。这句也常用 xorl %esi, %esi 这样的指令,都是置为零
movl $0, %eax
addq $16, %rsp # 缩小栈
# 函数调用的尾声,恢复栈指针为原来的值
popq %rbp # 恢复调用者栈帧的底部数值
retq # 返回
# 文本段,保存字符串字面量
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "fun1 :%d \n"
其栈帧的变化过程,如下:
使用栈来传递参数时,需要将函数参数以逆序的方向入栈,并发出call
指令。调用后在将参数出栈:
printf("The numer is %d",88);
.section .data
test_string:
.ascii "The numer is %d\0"
.section .text
pushl $88
pushl $test_string
call printf
popl %eax
popl %eax
汇编语言中全局变量访问方式与局部变量不同。全局变量通过直接寻址访问,而局部变量使用基址寻址方式,例如
int my_global_var;
int foo()
{
int my_local_var;
my_local_var = 1;
my_glocal_var = 2;
return 0
}
用汇编表示以上为:
.section .data
.lcomm my_globl_var, 4
.type foo, @function
foo:
pushl %ebp #保存原栈基址
movl %esp, %ebp #令栈指针指向新基址指针
subl $4, %esp #为变量my_local_var保留空间
.equ my_local_var, -4 #用my_local_var寻找局部变量
movl $1, my_local_var(%ebp)
movl $2, my_global_var
movl %ebp, %esp #清除函数变量并返回
popl %ebp
ret
指针,它只是保存某个值的地址。全局变量:
int global_data = 30;
其对应的汇编为:
.section .data
global_data:
.long 30
C语言中取地址如下:
p = &global_data;
对应的汇编为:
movl $global_data, %eax
可以看到汇编语言中总是通说指针访问内存,也就是直接寻址方式,为了取得指针本身,必须采用立即寻址方式。
局部变量略为复杂,C语言代码如下:
void foo()
{
int a;
int *b;
a = 30;
b = &a;
*b = 44;
}
对应汇编如下:
foo:
#标准函数开头
pushl %ebp
movl %ebp,%esp
#保留两个字的内存
subl -8, %ebp
.equ A_VAR, -4
.equ B_VAR, -8
#a = 30
movl $30, A_VAL(%ebp)
#b = &a
movl $A_VAR, B_VAR(%ebp)
addl %ebp, B_VAR(%ebp)
#*b = 30
movl B_VAR(%ebp), %eax #B
movl $30, (%eax)
#标准结束函数
movl %ebp ,%esp
popl %ebp
ret
要获取局部变量的地址,必须按基址寻址方式计算该地址。还有更简单的方式就是lea指令,该指令加载有效地址,会让计算机计算地址,然后在需要的时候加上地址:
#b = &a
leal A_VAR(%ebp), %eax
movl %eax, B_VAR(%ebp)
结构时对内存块的简单描述,例如,在C语言中可以使用如下代码:
struct person{
char pristname[40];
char lastname[40];
int age
};
汇编中只是给予你一种使用84字节数据的方式。
.equ PERSON_SIZE, 84
.equ PERSON_FIRSTNAME_OFFSET, 0
.equ PERSON_LASTNAME_OFFSET, 40
.equ PERSON_AGE_OFFSET, 80
当声明此类型的一个变量时,保留84字节空间就行,C代码如下:
void foo()
{
struct person p;
/**/
……
}
对应的汇编代码如下:
foo:
#标准开头
pushl %ebp
movl %esp, %ebp
#为局部变量分配空间
subl $PERSON_SIZE, %esp
#这是变量相对于%ebp的偏移量
.equ P_VAR, 0-PERSON_SIZE
……
#标准结束
movl %ebp, %esp
pop %ebp
ret
访问结构体成员,必须使用基址寻址方式,偏移量为上面定义的值。如C语言设置年龄如下:
p.age = 30;
对应的汇编入下:
movl $30, P_VAR + PERSON_AGE_OFFSET(%ebp)
C语言语句如下:
while (a < b){
/*某些操作*/
}
/*结束循环*/
这些对应的汇编如下所示:
loop_begin:
movl a, %eax
movl b, %ebx
cmpl %eax, %ebx
jge loop_end
loop_body:
#某些操作
jmp loop_begin
loop_end:
#结束循环
上面说到寄存器%ecx可用作计数器,终止条件为0,loop 指令会递减%ecx,并在%ecx不为0 的条件下跳转到指定地址,例如,需执行某个语句100次C 语言如下:
for (i = 0; i < 100; i++){
/*某些操作*/
}
汇编实现如下:
loop_initalize:
movl 100,%ecx
loop_begin:
#某些操作
#递减%ecx,若%ecx不为0则继续循环
loop loop_begin
rest_of_program:
if(a == b){
/*真分支操作*/
}else{
/*假分支操作*/
}
/*真假汇合*/
在汇编中表示如下:
#将a.b移入寄存器用于比较
movl a, %eax
movl b, %ebx
#比较
cmpl %eax, %ebx
#跳转到真分支
je true_branch
fale_branch: #非必要标签,只是为了说明这是假分支
#假分支代码
#跳到真假汇合
jmp reconverge
true_branch:
#真分支代码
reconverge:
之前我们用的例子都是采用整数,现在使用浮点数来做运算。下面这段代码:
float fun1(float a, float b){
float c = 2.0;
return a + b + c;
}