Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
3408 字
17 分钟
Assambly and C function

简介#

本篇介绍基本的汇编语言和对应的C语言结构。包括if,while和switch,以及最重要的函数function。

NOTE

本节的汇编语法以MASM-x86 64架构举例

汇编语言#

指令集#

CPU是用来处理数据的。其处理数据所依赖的方式是执行对应的机器指令(微指令)。而指令集就是CPU对外暴露的接口。它定义了可用的机器指令,寄存器合集,寻址方式和异常与中断语义。

因此,不同指令集的CPU使用不同的机器语言,这也对应了不同汇编语言。

目前常见的架构是:x86/MIPS/ARM以及对应的64位版本

汇编与机器语言#

CPU实际执行的是机器语言(所对应的微指令),即执行的是二进制代码,类似于:55 81 ec 00 01 00 00 89 e5

这些代码是基于对应架构的,即x86/arm/arch64/PowerPC等的机器代码均不相同,这表明CPU使用的寄存器/操作内存的方法在不同架构间也是不同的。

而汇编代码,是对应机器语言的简化形式,用字符来标记某些特定的指令,简化写法。

同时,不同的汇编语言可以由不同汇编器生成同一种机器语言。例如:AT&T和MASM和INTEL语法均可由不同汇编器生成x86架构的机器代码。

汇编入门#

计算机基本存储架构:CPU->寄存器->缓存->内存->外存

也就是说,当CPU需要数据时,先从寄存器中获取,如果没有,在缓存中寻找并放入寄存器。如果缓存未找到,就在内存中寻找内存。内存数据可以直接放入寄存器,也可能会用某种缓存刷新算法放入缓存。如果内存也没有,则会通过换页算法将外存内容放入内存,再放入寄存器供CPU使用。

因此,作为CPU执行的接口,肯定会提供操作寄存器和内存的指令来实现读写。同时,CPU最主要的功能是运算,因此也会有运算相关的指令。

以下,就先从寄存器开始讲起。

寄存器#

在x86 架构中,提供了许多寄存器供软件使用。分为:

  • 通用寄存器

  • 段寄存器

  • 指令指针寄存器

  • 标志寄存器

  • 控制寄存器

  • 调试寄存器

  • 浮点寄存器

  • 系统寄存器

一般来说:汇编中常见的寄存器是通用寄存器,指令指针寄存器,标志寄存器和浮点寄存器。所以本文仅介绍以上寄存器。

CPU是通过外总线和内存进行数据交换。而总线宽度表明了当前计算机是多少位(C语言指针那一节说过)。因此,寄存器的大小和总线宽度相关。

通用寄存器#

在16位的时候,存在这些通用寄存器:

AX,BX,CX,DX,SI,DI,BP,SP这几个寄存器每个都有一个16位长度用来存放数据。且其中的内容均可自定义,因此,被叫做通用寄存器。为了实现更精准的按字节操作,又为前4个寄存器提供了l和h后缀(比如AL和BH,low和high)用来访问寄存器低8位和高8位。(详细内容见后表)。 其中A,B,C,D无实际意义。 SI指sorce index 源地址索引寄存器, DI指destination index 目的地址索引寄存器, BP指base pointer 栈基地址指针寄存器 SP指stack pointer 栈指针寄存器

在32位时代,为寄存器添加了E前缀,表示extend来说明这是扩展为32位的寄存器。比如:EAX,EBX,ECX,EDX,ESI,EDI,ESP,EBP

在64位时代,为寄存器添加了R前缀,表示register来说明这是64位寄存器(也许Intel也不知道该用什么表示64位了)。同时从8个通用寄存器扩展到了16个(后面的命名就很简单了,显得很草率)。特别的,新添加的寄存器只支持了低16位的低8位的访问,没有支持高8位的访问。也许是为了整齐一点,为之前的SI,DI,BP,SP也添加了低8位访问。

通用寄存器表格如下:

64位寄存器32位别名16位别名8位别名用途说明
RAXEAXAXAL/AH累加器,常用于算术运算结果
RBXEBXBXBL/BH基址寄存器,常用于存储数据地址
RCXECXCXCL/CH计数寄存器,常用于循环、移位操作
RDXEDXDXDL/DH数据寄存器,常用于乘除法、I/O
RSIESISISIL源索引寄存器,常用于字符串/数组操作
RDIEDIDIDIL目的索引寄存器,常用于字符串/数组操作
RBPEBPBPBPL基址指针,常用于栈帧管理
RSPESPSPSPL栈指针,指向当前栈顶
R8R8DR8WR8B
R9R9DR9WR9B
R10R10DR10WR10B
R11R11DR11WR11B
R12R12DR12WR12B
R13R13DR13WR13B
R14R14DR14WR14B
R15R15DR15WR15B

其中,32位时只能使用前8个寄存器且没有64位别名,16位时只可使用前8个寄存器且没有64位和32位别名。同时可以看到上面有一个用途说明,这是说明他们可能和某些汇编指令的输入和输出有关。

指令指针寄存器#

在x86架构中,是IP寄存器,Instruction pointer 指令指针寄存器。存放一个地址(所以大小和位数相同)。始终指向下一条指令的地址。同时,由于不是通用寄存器,不可以以正常的方式读写,也不能使用。不过在64位时,添加了RIP相对寻址的方式来使用地址。

标志寄存器#

每个机器代码运行后,都会产生一个标志表示这个代码的执行结果。

举个例子:add ax,bx表示a += b,此时ax应当是其原值+bx的值。但是如果ax是0xffff,bx是0x1,最后的ax会变为0,因为ax最大保存16位,高位无法继续进位了,因此执行后就会设置CF标志为1(carry flag),表示运算结果发生了高位进位。

整个flag组是一个32位的寄存器,被叫做rflags

d5cadb929fe7499eba20fc81eb76ce48

具体含义如下:

Flag中文名含义
CF进位标志无符号加法产生进位或减法产生借位
PF奇偶标志结果最低字节中 1 的个数为偶数时置位
AF辅助进位标志低半字节(位3→位4)有进/借位,用于 BCD 运算
ZF零标志运算结果为 0 时置位
SF符号标志结果最高位(符号位)为 1 时置位,表示负数(补码)
TF陷阱标志单步调试模式(TF=1 时逐条执行)
IF中断允许标志控制可屏蔽中断是否被响应(1=允许)
DF方向标志字符串指令处理方向(0=递增,1=递减)
OF溢出标志有符号运算结果超出可表示范围时置位
IOPLI/O 特权级两位字段,表示当前任务的 I/O 权限级别
NT嵌套任务任务切换时指示嵌套任务链
RF恢复标志用于调试,控制某些异常的重新触发
VM虚拟 8086 模式指示处理器处于虚拟 8086 模式
AC对齐检查启用内存对齐检查
VIF / VIP虚拟中断标志 / 虚拟中断挂起用于虚拟化环境管理中断
ID标识标志允许通过 CPUID 指令检测处理器特性

浮点寄存器#

一般寄存器都是32位或者64位,但是浮点寄存器可以达到128位或以上:

XMM,YMM,ZMM寄存器组中XMM015是128位。YMM015是256位。ZMM0~31是512位。

这个寄存器在浮点计算和向量赋值时经常使用。

基本语法#

上面看完了基本的寄存器,这里来看CPU如何使用这些内容吧

一般来说,一条汇编语句分为操作码和操作数,操作码一定有,操作数不一定。

以下是数据传输指令:(这里只是介绍)

mov dest,src;表示将src放入dest中,dest可以是寄存器也可以是地址。
mov eax,0x1;
mov rax,rbx;
mov rax,[rbx];将rbx的内容作为地址,在地址中取值放到rax中。
lea rax,[rbx];第二操作数必须存在地址,将第二操作数的地址放到第一操作数中(因为用mov相当于自动解引用)
movzx rax,0x1;自动零扩展0x1到64位
movsx rax,0x1;自动带符号扩展0x1到64位

基本运算:

add rax,3;
sub rax,rbx;
mul ebx;高位在edx,低位在eax
idiv ecx;商在eax,余数在edx
xor rax,rax;
and rbx,rax;
inc rax;rax++
dec rbx;rbx--
shl rax,1;rax<<1 shift left
shr rax,1;rax>>1 shift right
sal rax,1;算数左移,shift arithmetic left,左移补0,高位进CF
sar rax,1;算数右移,shift arithmetic right,右移,高位不变,低位进CF
; 以下格式同上
rol;循环左移 rotate left
ror;循环右移 rotate right
rcl;带进位的循环左移 rotate through carry left
rcr;带进位的循环右移 rotate through carry right

栈操作相关:

sp寄存器在多数情况下都指向栈顶,使用push和pop可以操作sp寄存器

;将数据入栈,sp寄存器自动减少
push rax;
push 1;
;将数据出栈,sp寄存器自动增加
pop rax;

大致最基础的就这么多

控制流修改部分在结构里面一起说吧。

常见结构#

顺序结构就不需要说了,因为只要没有改变rip的值就是顺序的。

选择结构#

常见的选择结构是使用cmp配合条件跳转指令实现选择

if(x>0){
x=0;
}
else{
x=1;
}
mov rax,[rsp+0x10];假设x局部变量的位置在rsp+0x10处,此处将x放入rax
cmp rax,0;cmp指令假装执行x-0并设置对应标志位。
jg A;jmp if greater 如果rax大于0则rip跳转到对应位置执行,跳转条件由上方标志位设置
mov rax,1
jmp B;直接跳转
A:
mov rax,0;
B:
mov [rsp+0x10],rax;写回局部变量到内存

另一种实现方式是条件mov

mov rax,[rsp+0x10];
xor rcx,rcx;将rcx置零
mov rdx,1;
cmp rax,0;
cmovg rdx,rcx;条件mov指令,conditional move if greater 如果rax大于0,就将rcx移入rdx。
mov [rsp+0x10],rdx;将rdx写入

循环结构#

一般使用cx作为计数器配合jnz使用,可在开头也可在结尾。

void map_pos_to_zero(const long *a, long *b, size_t n) {
for (size_t i = 0; i < n; ++i) {
    long x = a[i];
    if (x > 0)
    b[i] = 0;
    else
    b[i] = 1;
    }
}
test rcx, rcx
jz .done
.loop:
mov rax, [rdi] ; rax := a[i]
xor r8, r8 ; r8 := 0 (then value)
mov r9, 1 ; r9 := 1 (else value)
cmp rax, 0 ; compare x,0 (signed)
cmovg r9, r8 ; if x>0 (signed) r9 := 0 else r9 stays 1
mov [rsi], r9 ; b[i] := r9
add rdi, 8 ; 增加目的指针索引
add rsi, 8 ; 增加源地址索引
dec rcx;该代码在rcx等于0时置ZF为1
jnz .loop;ZF不为1时跳转
.done:

SWITCH#

跳转多时使用跳转表实现:

switch (v) {
case 10: goto case10;
case 11: goto case11;
case 12: goto case12;
case 13: goto case13;
case 14: goto case14;
case 15: goto case15;
default: goto default_case;
}
case10:
return;
case11:
return;
case12:
return;
case13:
return;
case14:
return;
case15:
return;
default_case:
return;
}
mov rax, rdi ; rdi = switch value
sub rax, 10 ; 基为10
cmp rax, 5 ; 如果大于15直接跳到default
ja .L_default
lea rbx, [rel .jump_table];获取表入口
mov rdx, [rbx + rax*8] ;
jmp rdx;jmp可以用寄存器索引,寄存器值为目标地址。
.jump_table:
dq .L_case0
dq .L_case1
dq .L_case2
dq .L_case3
dq .L_case4
dq .L_case5

函数结构#

函数是基于栈的,此处的函数结构,是指如何调用,如何传参,如何平栈,如何存取局部变量。

调用方法#

汇编中,调用一个函数使用call和ret。

call是指,压入rip的值到栈顶,然后rip设置到对应的函数开头。 ret是指,将栈顶的值设置到rip。

通过这种方法来实现函数的切换。

调用约定#

在x86 64 windows下,只有一种调用约定,即前4个参数放在rcx,rdx,r8,r9,剩余的参数从右至左逆序入栈。被调用者清理堆栈。同时,如果函数有返回值,则均放在rax寄存器中。

类似于这样:

; 假设参数为 a1..a6(均 64-bit),调用 foo(a1,a2,a3,a4,a5,a6)
sub rsp, 10 ; 先减出来,之后使用mov入栈
mov rcx, rax ; a1 -> RCX
mov rdx, rbx ; a2 -> RDX
mov r8, rcx ; a3 -> R8
mov r9, rsi ; a4 -> R9
mov qword [rsp+8], r8 ; push a6 (最右) 到栈 (offset 32 为第6)
mov qword [rsp], rdx ; push a5 到栈
; 注意:上面用 mov 写入栈代替 push 以保持对齐/效率
call foo
; 继续操作

函数结构#

该结构为常见结构,但是也不一定需要完全遵循。

foo:
push rbp ; 保存调用者函数的栈基址
mov rbp, rsp ; 设置栈基址为当前栈顶
sub rsp, 32 ; 局部空间(保持对齐),使用了多少最后就恢复多少。用于存放局部变量
    push rbx ; 保存非易失寄存器,这里只保存想保存的,顺序可以和上一个交换,主要目的是最后返回上一个栈帧的时候可以还原寄存器
; 函数主要逻辑
    ; 如果有
pop rbx ; 还原寄存器
add rsp, 32 ; 减回栈
pop rbp            ; 还原栈基址
ret                    ; 返回

不同的编译器可能会额外对栈和局部变量区进行处理。比如MSVC会在执行函数具体指令前,将栈内容全部填充为0xCC。


基本的内容就这么多,本节的内容建议打开IDA一边看对应的结构一边理解。

Assambly and C function
https://pri87.vip/posts/assambly-and-c-function/
作者
pRism
发布于
2026-01-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00