计算机系统结构
为了使用汇编语言编程,就必须要了解计算机的体系结构。
处理器
处理器是计算机的大脑,它执行数据运算、逻辑与控制的操作。它执行程序指令,与IO设备、内存等进行交互操作。
寄存器
寄存器是处理最直接使用的存储单元,处理器可以在一个时钟周期内访问寄存器。
80186、80286、80386以及后续的Pentium系列称为x86或者80x86。在80386及其之后的处理器称为I386,它们是32位的处理器。
通用寄存器包括EAX、EBX、ECX、EDX、EBP、ESI、EDI、ESI。
EAX - Accumulator Register,用于保存一些操作的操作数。
EBX - Base Register,保存数据段数据的指针。
ECX - Counter Register,用于循环操作和字符串操作。
EDX - 用于指向IO端口的指针。
ESI - Source Index,用于字符串操作的源字符串的指针,也可以作为数据段(DS)的指针。
EDI - Destination Index,用于字符串操作的目的字符串的指针,也可以作为扩展段(ES)的指针。
ESP - Stack Pointer,总是指向栈顶。
EBP - Base Pointer,总是指向栈基。
FLAGS用于指示CPU的状态,或者最近一次操作的状态。
Carry Flag,当计算产生进位时,被置为1。
Zero Flag,当计算结果为0时,被置为1。
Sign Flag,当计算结果为负数时,被置为1。
Parity Flag,当计算结果为奇数时,被置为1。
Interrupt Flag,当设置为1时,只接受外部中断。
EIP是指令指针(Instruction Pointer),它指向下一条要执行的指令。在内存中只有两类东西,数据和程序。当启动一个程序时,会将它们加载到内存中,然后让EIP指向这个程序的入口地址,然后顺序执行,除非遇到了分支语句。
段寄存器,在32位的处理器中只用于访问描述符表。
总线
分为数据总线、地址总线、控制总线。
系统时钟
处理器的速度依赖于系统时钟的速度。
中断
中断可以是外部产生,也可以是内部产生。在基于Linux的系统上08H中断和基于Windows的21H中断,都是操作系统产生的,用于实现系统调用。
当中断产生时,处理器暂停当前工作,操作寄存器的值到内存中,然后执行中断处理程序,中断处理程序存储在中断向量表中。执行完中断处理程序,处理器恢复中断之前的寄存器继续执行。
如何开始
计算机语言可以分为三类:机器语言、汇编语言、高级语言。
安装NASM
到
http://www.nasm.us这里下载NASM,并安装。
为什么要学习汇编语言
第一,学习汇编语言,让你更了解计算机的组成,并知道程序是如何执行的。
第二,汇编语言比高级语言效率更高,代码规模更小。在嵌入式系统中使用较多。
第三,Linux内核及一些系统软件中使用汇编,在C和C++等语言中可以嵌入汇编语言。
第一个程序
第一个程序当然是打印Hello World,将如下程序保存为hello.asm
; Program to print "Hello World" ; Section where we write our program section .text global _start: _start: mov eax, 4 mov ebx, 1 mov ecx, string mov edx, length int 80h ; System Call to exit mov eax, 1 mov ebx, 0 int 80h ; Section to store initialized variabled section .data string: db 'Hello World', 0Ah length: equ 12 ; Section to store uninitialized variabled section .bss var: resb 1
然后执行如下命令:
$ nasm -f elf64 hello.asm这是在Linux 64位系统下的格式,如果是32位系统,使用elf32。上面的命令将生成hello.o文件。然后链接成可执行文件:
$ ld -s hello.o -o hello将生成hello的可执行文件。
调试程序
如果希望程序中包含调试信息需要在编译时添加-g参数,如:
$ nasm -g -f elf64 hello.asm链接参数不变,然后在使用gdb进行调试时,就可以了。
在调试汇编时,使用ni或者nexti表示执行下条指令,而平时使用的next为下一语句,注意区别。使用info registers查看寄存器信息。使用disassemble进行反汇编,其中=>所指向的指令是下一条要执行的指令。如:
Dump of assembler code for function _start: => 0x00000000004000b0 <+0>: mov $0x4,%eax 0x00000000004000b5 <+5>: mov $0x1,%ebx 0x00000000004000ba <+10>: mov $0x6000d4,%ecx 0x00000000004000bf <+15>: mov $0xc,%edx 0x00000000004000c4 <+20>: int $0x80 0x00000000004000c6 <+22>: mov $0x1,%eax 0x00000000004000cb <+27>: mov $0x0,%ebx 0x00000000004000d0 <+32>: int $0x80 End of assembler dump.
有了调试功能,对于理解汇编语言及其执行过程会更加的方便。调试在手,天下我有。
程序解析
NASM程序包含不同的section,主要有:
Section .text - 为程序的可执行代码。
Section .bss - 声明没有初始值的变量。
Section .data - 声明需要初始值的变量。
RESx指令用于声明内存空间,不需要初始值。
Dx指令用于声明内存空间,同时提供初始值。
x | 含义 | 字节数 |
b | Byte | 1 |
w | Word | 2 |
d | Double word | 4 |
q | Quad word | 8 |
t | Ten word | 20 |
如:
var1: resb 1 ; 1个字节 var2: dw 25 ; 1个字,初始化为25
; - 在NASM中用于表示注释的开始。
01110b - 表示二进制数。
31h - 表示十六进制数。
123o - 表示八进制数。
同时声明多个元素(数组)
var: db 1, 2, 3 ; 3个字节分别保存1, 2, 3字符串:
string: db "Hello" string2: db "H", "e", "l", "l", "o"两个字符串是等价的,都是5个字节长度。
TIMES指令用于声明多个具有相同初始值的数组,如:
var: times 10 db 1声明10个字节空间,同时都初始化为1。
NASM中的引用机制,如果某些操作的操作数在内存当中,需要它的它的地址来使用操作数,而地址或者在变量当中,或者在寄存器中。不防假设这个地址在变量当中,得到地址后需要进行解引用操作,在NASM中使用[ ]。如:
mov eax, [label] ; label所引用地址的变量值被复制到eax中 mov eax, label ; label所指地址被复制到eax中
mov dword[ebx], 1 inc BYTE[label]
X86基本指令集
MOV - MOVE/COPY
复制寄存器或者内存的值到另一个寄存器或者内存,或者使用立即数设置寄存器值或者内存。
mov dest, srcsrc - 可以是寄存器或者内存地址。
src和dest不能同时都是内在地址。
MOVZX - Move and Extend
从较小长度值复制到较大长度值时,使用零扩展,即高位补0。
movzx dest, srcdest的空间大小要>=src的空间大小。
MOVSX -
为符号扩展,即高位补符号位。
movzx dest, src
CBW CWD - Convert Byte to Word and Convert Word to Double
符号扩展。
cbw cwd
CBW - 扩展AL到AX。
CWD - 扩展AX到DX:AX。
ADD - Addition
加法操作。
add dest, src ; dest = dest + src
两个操作数必须相同大小。
SUB - Substraction
减法操作。
sub dest, src ; dest = dest - src
INC - Increment operation
将内存或者寄存器的值加1。
inc eax inc byte[var]
DEC - Decrement operation
将内存或者寄存器的值减1。
dec eax dec word[var]
MUL - Multiplication
乘法操作。
mul src
如果src是1个字节长度,则AX = AL * src。
如果src是1个字长度,则DX:AX = AX * src。
如果src是2个字长度,则EDX:EAX = EAX * src。
IMUL - Multiplication of signed numbers
有符号数的乘法操作。
imul src imul dest, src imul dest, src1, src2
第一种形式与mul相同。
第二种形式dest = dest * src。
第三种形式dest = src1 * src2。
DIV - Division
除法操作。
div src
根据src长度的不同采用EDX:EAX/DX:AX/AX去除以src。具体规则为:
如果src是1个字节长度,用AX除以src,AH为余数,AL为商。
如果src是1个字长度,用DX:AX除以src,DX为余数,AX为商。
如果src是2个字长度,用EDX:EAX除以src,EDX为余数,EAX为商。
NEG - Negation of signed numbers
取负数。
neg op1
将内存或者寄存器操作数取负数。
CLC - Clear Carry
清除进位标志(CF)。
clc
ADC - Add with Carry
带进位的加法。
adc dest, src
SBB - Subtract with Borrow
带借位的减法。
sbb dest, src
JMP - Unconditionally Jump to label
无条件中转指令。相当于C语言中的goto。
label: ; some code jmp label
CMP -- Compares the Operands
比较操作。
cmp op1, op2
相当于计算op1 - op2,但不会保存计算结果,而只影响CPU的标志寄存器。如果op1 == op2,则ZF将被置为1。
JZ - Jump if Zero Flag is Set
JNZ - Jump if Zero Flag is Unset
JC - Jump if Carry Flag is Set
JNC - Jump if Carry Flag is Unset
JP - Jump if Parity Flag is Set
JNP - Jump if Parity Flag is Unset
JO - Jump if Overflow Flag is Set
JNO - Jump if Overflow Flag is Unset
JE - Jump if op1 == op2
JNE - Jump if op1 != op2
JA - Jump if above, if op1 > op2, for Unsigned number
JNA - Jump if not above, if op1 <= op2, for Unsigned number
JB - Jump if below, if op1 < op2, for Unsigned number
JNB - Jump if not below, if op1 >= op2, for Unsigned number
JG - Jump if greater, if op1 > op2, for Signed number
JNG - Jump if not greater, if op1 <= op2, for Signed number
JL - Jump if lesser, if op1 < op2, for Signed number
JNL - Jump if not lesser, if op1 >= op2, for Signed number
LOOP
loop label循环指令使用ecx作为循环变量,循环指令首先将ecx减少1,然后检查它是否不为零,如果不为零,则跳转到label所代表的指令处,否则跳到下一条指令执行。
AND - Bitwise Logical AND
按位与操作。
AND op1, po2
结果:op1 = op1 & op2。
OR - Bitwise Logical OR
按位或操作。
OR op1, op2
XOR - Bitwise Logical Exclusive OR
按位异或操作。
XOR op1, op2
结果:op1 = op1 ^ op2。
NOT - Bitwise Logical Negation
按位取反操作。
NOT op1
TEST - Logical AND, affects only CPU FLAGS
TEST op1, op2进行逻辑AND操作,但不保存运算结果,只影响标志寄存器。使用的方法与CMP类似。
SHL - Shift Left
左移位操作。SHL op1, op2op1可以是寄存器或者内存变量,op2必须是立即数。相当于,op1 = op1 << op2。结果中右侧会补0。
SHR - Shift Right
右移位操作。
SHR op1, op2op1可以是寄存器或者内存变量,op2必须是立即数。相当于,op1 = op1 >> op2。结果中左侧会补0。
ROL - Rotate Left
循环左移位操作。
ROL op1, op2op1可以是寄存器或者内存变量,op2必须是立即数。
ROR - Rotate Right
循环右移位操作。
ROR op1, op2op1可以是寄存器或者内存变量,op2必须是立即数。
RCL - Rotate Left with Carry
带进位的循环左移位操作。
RCL op1, op2op1可以是寄存器或者内存变量,op2必须是立即数。将CF位作为op1的最左一位进行循环左移位。
RCR - Rotate Right with Carry
带进位的循环右移位操作。
RCL op1, op2op1可以是寄存器或者内存变量,op2必须是立即数。将CF位作为op1的最左一位进行循环右移位。
PUSH - Pushes a value into system stack
入栈操作。PUSH reg/constPUSH减少ESP的值,然后将reg/const的值复制到系统栈。
POP - Pop off a value from the system stack
出栈操作。
POP regPOP将系统栈的值复制到reg,然后将增加ESP的值。
PUSHA - Pushes the value of all general purpose registers
PUSHA
将所有通用寄存器的值入栈,通常用于调用子程序。
POPA - POP off and restore the value of all general purpose registers
POPA将所有通用寄存器的值出栈,通常在结束调用子程序后由调用程序调用。
PUSHF - Pushes all the CPU FLAGS
PUSHF
POPF - POP off and restore the CPU FLAGS
POPF
%DEFINE - Pre-processor Directives in NASM
NASM的预处理命令,相当于C语言中的#define。
%DEFINE SIZE 100
NASM中的基本IO
Linux下,使用eax传递系统调用编号。
Exit
调用编号为1,成功退出传递参数0,即传递参数eax为1,ebx为0,所以:
mov eax, 1 mov ebx, 0 int 80h
Read
只参读取字符串或者字符。
系统调用编号为3,传递给eax寄存器。
标准输入设备的引用号为0,传递给ebx寄存器。
将用于存放读取字符串的内存地址,传递给ecx寄存器。
将所要读取最大字符数,传递给edx寄存器,不能大于ecx指向的内存空间大小。
触发80h中断。
读取的字符串存储在ecx所指向的内存,读取的字符数存储在eax寄存器。
mov eax, 3 mov ebx, 0 mov ecx, string mov edx, dword[length] int 80h
Write
只能写字符串或者字符。
系统调用编号为4,传递给eax寄存器。
标准输出设备的引用号为1,传递给ebx寄存器。
将用于存放要输出字符串的内在地址,传递给ecx寄存器。
将要输出的字符数,传递给edx寄存器。
触发80h中断。
实际写的字符长度存储在eax中。
mov eax, 4 mov ebx, 1 mov ecx, string mov edx, dword[length] int 80h
SubPrograms(子程序)
子程序是可重复调用的代码段,在高级语言中称为函数。
CALL & RET
在NASM中子程序实现涉及CALL和RET指令,基本结构为:
; main code ...... call func_name ...... ; sub_program func_name: ...... ret
当执行CALL指令时,会将EIP的值(即下条要执行的指令地址)入栈,将子程序的地址复制到EIP。这样接下来会执行子程序。
当子程序执行到ret指令时,会将栈顶值出栈,值被复制到EIP寄存器。很显然执行子程序时确保栈使用的正确,否则无法正确返回。
如,计算求和:
; Subprograms section .text global _start _start: mov ecx, 5 call sum mov ecx, 10 call sum mov eax, 1 mov ebx, 0 int 80h sum: mov eax, 0 loop1: add eax, ecx loop loop1 ret
调用规则
调用程序和子程序之间传递参数的方式被称为调用规则。参数传递可以使用系统寄存器、内存变量、系统堆栈。如果使用系统堆栈,应该在执行call指令之前将参数入栈,在子程序中确保返回地址在栈项。
使用gdb调试子程序时,可以使用stepi或者si进行入到子程序单步执行。使用nexti或者ni会跳过子程序单步执行。
递归调用
子程序调用自身来计算,称为递归调用。
在NASM中调用C库
可以在NASM中调用C语言库函数,但是要遵守C语言的调用规则:
参数的传递是通过栈进行,参数由右至左依次入栈。
C函数不会自动弹出参数,所以需要在调用完C函数时,清理栈。
如下调用C语言的printf函数:
; Equivlent to C code ; #include <stdio.h> ; int main() ; { ; char msg[] = "Hello world"; ; printf("%s\n", msg); ; return 0; ; } extern printf section .data msg: db "Hello world", 0 fmt: db "%s", 10, 0 section .text global main main: push rbp mov rdi, fmt mov rsi, msg mov rax, 0 call printf pop rbp mov rax, 0 ret
还剩下数组、字符串、浮点数的操作部分,以后补充。
参考
- Introduction to NASM A Study Material for CS2093 - Hardware Laboratory. csdn下载。
- 如果使用vim,添加autocmd BufRead,BufNewFile *.asm set filetype=nasm到~/.vimrc中,使得.asm文件默认语法高亮为nasm的语法格式。
- GDB调试汇编命令mohit。
- NASM官方文档。
- 64位环境NASM调用printf函数。