NASM汇编随笔

编译链接
nasm -f elf helloworld.asm
ld -m elf_i386 helloworld.o -o helloworld
./helloworld
符号约定
入口

类似于其他语言的main函数,gloabl _start是约定的NASM汇编代码入口:

SECTION .text
global _start

_start:
	; other codes
常量
org 0x7C00 ; 约定的引导扇区首地址
dw 0xAA55  ; 约定的引导扇区标志
0x0A	   ; 换行\n,即LF
0x0D	   ; 回车\r,即CR
系统调用
名称 eax ebx ecx edx
sys_exit 调用号1 错误码
int err_code
sys_read 调用号3 文件描述符
unsigned int fd
文件地址
char __user *buf
文件长度
size_t count
sys_write 调用号4 文件描述符
unsigned int fd
文件地址
const char __user *buf
文件长度
size_t count
sys_execve 调用号11 待执行文件
char __user *
参数数组
char __user * __user *
环境变量数组
char __user * __user *
sys_fork 调用号2 [核心栈]
[struct pt_regs*]
sys_time 调用号13 time_t __user *tloc
sys_create 调用号8 文件路径
const char __user *path
文件权限
int mode
sys_open 调用号5 文件
const char __user *fd
读写模式
int flags

int mode
sys_close 调用号6 文件描述符
unsigned int fd
sys_unlink 调用号10 文件路径
const char __user *path

__user表明参数是一个用户空间(user-space)的指针,不能在kernel代码中直接访问,需要经过检查;因为用户空间的内存是不可靠的。

char __user * __user *代表的是指向一个用户空间指针的一个用户空间指针。其实简单说来,就是char**,一个二维指针。

sys_write

STDOUT的文件描述符fd是1,算是一个约定吧。所以如果要在控制台输出,相当于是把输出的内容“写”到STDOUT这个“文件”里(大概这就是文件抽象的好处,设备可以看成文件,用统一的方式进行操作):

MOV ebx, 1
sys_read

同样的,STDIN的文件描述符fd是0:

MOV ebx, 0
sys_execve

注意,在传递参数数组的时候,需要将command包括进去:

SECTION .data
command         db      '/bin/echo', 0h
arg1            db      'Hello World!', 0h
arguments       dd      command
                dd      arg1
                dd      0h
environment     dd      0h                  
 
SECTION .text
global  _start

_start:
    mov     edx, environment
    mov     ecx, arguments
    mov     ebx, command
    mov     eax, 11
    int     80h
sys_fork

父进程中eax不为0,子进程中eax为0。

sys_create

返回的文件名放在寄存器eax里。

sys_open
含义 描述 标志
O_RDONLY open file in read only mode 0
O_WRONLY open file in write only mode 1
O_RDWR open file in read and write mode 2
寄存器
数据寄存器

AX = AH(Higher) + AL(Lower),Accumulator累加器(数值运算的默认寄存器)

BX = BH + BL,Base基址

CX = CH + CL,Counter计数器

DX = DH + DL,Data数据(一般用于存放除法的余数)

栈指针寄存器

SP 栈顶指针

BP 栈底指针

索引寄存器

SI 源寄存器

DI 目标寄存器

段寄存器

CS 代码段

DS 数据段

SS 堆栈段

ES 附加段

状态标志寄存器FLAG

进位标志CF(Carry Flag)

奇偶标志PF(Parity Flag)

奇偶标志PF用于反映运算结果中“1”的个数的奇偶性。如果“1”的个数为偶数,则PF的值为1,否则其值为0。利用PF可进行奇偶校验检查

零标志ZF(Zero Flag)

符号标志SF(Sign Flag)

符号标志SF用来反映运算结果的符号位,它与运算结果的最高位相同。运算结果为正数时,SF的值为0,否则其值为1。

溢出标志OF(Overflow Flag)

几个指令

EQU定义常量,类似于C中的#define

%include引入外部文件实现模块化,类似于#include

%include "another-module.asm"
地址抽象

标签(LABEL)和变量名、函数名都是地址(地址抽象?),直接导致取变量(在SECTION .data中声明,出现在寄存器里的不是变量,是常量)的值需要解引用(通过[]运算符)。可以显式声明其类型。比如:

byte[eax]

是用来获取eax寄存器里的数据的,eax里存的是byte类型的数据。

times

用times指令来进行指令的“循环”的经典代码,用0填充剩下的空间,直到510字节为止(一般用于引导扇区):

times 510-($-$$) db 0

概括下来,times的格式大概是这样:

times [次数] [指令]
条件跳转

类似于if的效果。事实上,这些是跟寄存器息息相关的:

单操作数
指令 条件 解释
JZ 结果为0 零标志寄存器ZF置位
JC 结果产生进位 进位标志寄存器CF置位
JP 结果的二进制表示中的“1”的个数为偶数 奇偶标志寄存器PF置位
JO 结果产生溢出 溢出标志寄存器OF置位
双操作数
指令 条件 解释
JE 相等 Equals
JA (无符号)大于 Above
JB (无符号)小于 Below
JG (有符号)大于 Greater
JL (有符号)小于 Lesser

还有对应的NOT指令,在中间加个N即可(比如JNZ表示结果非0)。

注意事项
程序崩溃

调用sys_exit了吗?

Program received signal SIGSEGV, Segmentation fault

原因是引用了不存在的地址。

  • 调用sys_exit了吗?

  • 是不是把常量当成了指针?比如:

    mov eax, 0x30	; eax='0'
    mov edx, 1		
    mov ecx, eax	; ERROR
    mov ebx, 1		; STDOUT
    mov eax, 4		; sys_write
    int 0x80
    

    正确的做法应该是将常量压入栈中,引用常量的地址:

    mov eax, 0x30	; eax='0'
    mov edx, 1		
    push eax
    mov ecx, esp	; here esp refers to the address of '0x30'
    mov ebx, 1		; STDOUT
    mov eax, 4		; sys_write
    int 0x80
    
push不改变源寄存器的值

PUSH指令只会往栈中压入一个值,并不会影响原寄存器的值。比如这条指令:

PUSH eax

eax并不会因为被push了就被清空,或者变成别的什么值。这一步唯一的影响,就是栈里多了一个等同于eax的值。

参数传递

可以通过数据寄存器(比如eax)来进行参数传递;如果参数多了,超过了数据寄存器的数量,可以考虑用栈进行参数传递,这个时候就需要用到栈顶指针esp了。

字符串结尾的空字符

因为在内存中数据是连续排列的,没有空字符无法区分两个字符串,所以C风格字符串需要'\0'作为字符串结尾,这是来自汇编的legacy。

数值运算

加减法都是两个操作数,但乘除法是一个操作数。

什么意思呢?看一下例子就知道了:

add	eax, ebx	; eax + ebx
sub eax, ebx	; eax - ebx
mul ebx			; eax * ebx
div ebx			; eax / ebx

这些运算有有符号和无符号之分。以除法为例,div是无符号除法,idiv是有符号除法。

除法还有一个特殊之处,就是默认将eax里的值当成被除数,并且将商放在eax里,余数放在edx里。这算是一个约定,就像乘法默认把结果放到eax里一样。

除零异常

有时候编译时会提示除零异常,但代码逻辑上是完全没有错误的。这个时候可能需要检查一下在除法运算前是否清空了edx寄存器。如果没有清空edx寄存器,可能就会导致除零异常。

至于原因,就得涉及到计算机组成原理了。

被除数在eax里,余数在edx里,计算的时候会进行拼接,变成edx:eax进行除法运算。所以,如果edx没有清空,会造成不可预料的计算结果。

A、M、Q寄存器,被除数

清空寄存器

下面两行代码都能清空ebx寄存器。它们有区别吗?

xor	ebx, ebx
mov	ebx, 0

显然是有区别的。xor只需要2字节(操作码1字节,寄存器地址1字节),但mov需要5字节(操作码1字节,2个立即数4字节)。

既然都用汇编了,为什么不用内存占用更小的指令呢?

字符串也是立即数

我觉得是因为字符也是数字的一种。

数组名是地址

因为数组事实上是连续排列的一块内存,数组名只是指向首地址的指针罢了。

同样的,二维数组的名称就是指向指针的指针。这样做只是为了从逻辑上分开,但在内存里没啥区别,还是连续的一大块地址。

需要注意的是,数组的结尾也需要是一个空字符(0H)。

代码风格
  • 所有指令和寄存器名称都小写
  • 立即数的后缀大写
  • 0x中的x小写
  • 全局label驼峰命名
  • 局部label全小写
汇编IDE——SASM

高配版notepad++。

编译和链接配置

In Linux, just replace “gcc” to “ld”.

This linker options should be replaced by “$PROGRAM.OBJ$ -g -o $PROGRAM$”.

这是官方文档的说法,可能稍微有点抽象。具体的使用方法,进入设置->构建,然后按照上面的要求改一下就行了。参考配置如下图:
NASM汇编随笔_第1张图片
至于设置在哪里?可以熟悉一下Ubuntu的使用方法(

在这里使用到的变量的含义如下:

$SOURCE$ - 源代码文件

$LSTOUTPUT$ - 用于调试的文件

$PROGRAM.OBJ$ - 生成的.o(或者.obj)文件

$PROGRAM$ - 生成的可执行文件

如果要在64位上运行,需要将汇编选项里的elf32改成elf64,保存设置后重启SASM。如果不重启,可能无法应用设置。

2019.10.17 增加了官方文档地址

可以参考官方文档。地址:https://github.com/Dman95/SASM/wiki/Help-(English)#building-system-settings

调试

和别的IDE基本相同,也有断点和单步;除非你像我一样至今不会debug。

你可能感兴趣的:(操作系统,NASM,汇编)