计算机体系结构与NASM入门

计算机系统结构

为了使用汇编语言编程,就必须要了解计算机的体系结构。

处理器

处理器是计算机的大脑,它执行数据运算、逻辑与控制的操作。它执行程序指令,与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中

如果不能推断出所需要的值的大小,需要指定类型,使用:BYTE,WORD,DWORD,QWORD,TWORD。如:
mov dword[ebx], 1
inc BYTE[label]

X86基本指令集

MOV - MOVE/COPY

复制寄存器或者内存的值到另一个寄存器或者内存,或者使用立即数设置寄存器值或者内存。
mov dest, src
src - 可以是寄存器或者内存地址。
src和dest不能同时都是内在地址。

MOVZX - Move and Extend

从较小长度值复制到较大长度值时,使用零扩展,即高位补0。
movzx dest, src
dest的空间大小要>=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

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

用EAX/AX/AL的值乘寄存器或者内存操作数。具体规则为:

如果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

dest = dest + src + CF

SBB - Subtract with Borrow

带借位的减法。

sbb dest, src

dest = dest - src - CF


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

结果:op1 = op1 | op2。

XOR - Bitwise Logical Exclusive OR

按位异或操作。
XOR op1, op2

结果:op1 = op1 ^ op2。

NOT - Bitwise Logical Negation

按位取反操作。
NOT op1

结果:op1 = ~op1。

TEST - Logical AND, affects only CPU FLAGS

TEST op1, op2
进行逻辑AND操作,但不保存运算结果,只影响标志寄存器。使用的方法与CMP类似。

SHL - Shift Left

左移位操作。
SHL op1, op2
op1可以是寄存器或者内存变量,op2必须是立即数。相当于,op1 = op1 << op2。结果中右侧会补0。

SHR - Shift Right

右移位操作。
SHR op1, op2
op1可以是寄存器或者内存变量,op2必须是立即数。相当于,op1 = op1 >> op2。结果中左侧会补0。

ROL - Rotate Left

循环左移位操作。
ROL op1, op2
op1可以是寄存器或者内存变量,op2必须是立即数。

ROR - Rotate Right

循环右移位操作。
ROR op1, op2
op1可以是寄存器或者内存变量,op2必须是立即数。

RCL - Rotate Left with Carry

带进位的循环左移位操作。
RCL op1, op2
op1可以是寄存器或者内存变量,op2必须是立即数。将CF位作为op1的最左一位进行循环左移位。

RCR - Rotate Right with Carry

带进位的循环右移位操作。
RCL op1, op2
op1可以是寄存器或者内存变量,op2必须是立即数。将CF位作为op1的最左一位进行循环右移位。

PUSH - Pushes a value into system stack

入栈操作。
PUSH reg/const
PUSH减少ESP的值,然后将reg/const的值复制到系统栈。

POP - Pop off a value from the system stack

出栈操作。
POP reg
POP将系统栈的值复制到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

在NASM中基本输入与输出使用的是系统调用read和write。在Linux系统下使用中断80h进行系统调用。应用程序提供系统调用号(放置在eax寄存器),与必要的参数,然后执行int 80h指令,进行系统调用,操作系统知道去相应的位置,一般是通用寄存器和栈,取得必要的参数,然后执行中断处理程序,将结果放在指定的位置,一般是eax,接着将使用权交回应用程序。

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函数

猜你喜欢

转载自blog.csdn.net/himayan46/article/details/78639301