Nov 25, 2022
NASM 概念与实战/踩坑记录
- NJU OS lab-1
1. NASM 与 GDB 调试
1.1. 安装与运行
在 Ubuntu 下可以通过 apt 安装
sudo apt install nasm |
安装后就可以对文件进行汇编了,以big_decimal.asm
为例
在 Ubuntu 下
nasm -f elf big_decimal.asm -o big_decimal.o |
ld
是 GUN 自带的链接工具,可以将目标文件链接起来,我们这里因为只有一个文件因此不需要额外的指令。
复习一下,我们将一个写好的 C 程序转化为一个可以在 Unix 内核机器上执行的文件,需要经历下面四个步骤:
- 预处理:处理 C 中的预处理命令,也就是#开头的那些,默认的生成文件格式为
.i
- 编译:将 C 程序编译为汇编语言, 默认的生成文件格式为
.s
,这里的.s
和我们的.asm
没什么区别 - 汇编:将汇编语言转化为机器码,默认的生成文件格式为
.o
- 链接:链接动态库和静态库
因为我们直接在写汇编程序,当然就不需要第一步和第二步了。
1.2. GDB 调试
说到 GUN,不得不提今天的另一个主角,那就是 GDB,GNU symbolic debugger,它是一个在 Unix 内核中广受好评的调试工具。
我们可以使用 GDB 来调试我们的汇编代码
nasm -f elf big_decimal.asm -o big_decimal.o |
接下来进入 gdb 界面,开始你的调试工作。
和前面说到的一样,使用gdb
打开你的文件就好了,因为gdb
要控制另一个进程,所以别忘了给它开权限。
可以在汇编文件中设置断点,并在你想停止的地方call
它,比如
break_demo: |
然后只要在gdb
中
b break_demo |
就可以让它在执行到b
的时候中止程序。
可以使用指令
i registers |
查看代码运行的位置和寄存器状态。
如果你的代码有stdout
的输出,可能会破坏这个 layout 的格局,这时候使用refresh
指令刷新它。
最后,可以使用x
指令查看你的内存状况,比如查看0x40201c
开始的 20 个 bits 的内存
x/20b 0x40201c |
2. NASM 详解
2.1. NASM 程序的结构
NASM 是基于行的。大多数程序由指令后跟一个或多个部分组成。行可以具有可选标签。大多数行都有一条指令,后跟零个或多个操作数。
通常,您将代码放在的部分中,.text
并将常量数据放在的部分中.data
。
2.2. NASM 语法
2.2.1. 基础指令
有数百条指令。您无法一次全部学习它们。从这些 start:
mov x,y |
x ← y |
---|---|
and x,y |
x ← x and y |
or x,y |
x ← x or y |
xor x,y |
x ← x xor y |
add x,y |
x ← x + y |
sub x,y |
x ← x – y |
inc x |
x ← x + 1 |
dec x |
x ← x – 1 |
syscall |
调用操作系统例程 |
db |
一个伪指令 声明字节, 这将是在内存中的程序运行时 |
cmp
做比较je
如果先前的比较相等则跳转。jne
(如果不等于则跳转)jl
(如果不等于则跳转)jnl
(如果不小于则跳转)jg
(如果大于则跳转)jng
(如果不大于则跳转)jle
(如果小于或等于则跳转)jnle
(如果不小于或等于则跳转)jge
(如果大于或等于则跳转)jnge
(如果不大于或等于则跳转)equ
实际上不是真正的指令。它只是定义了供汇编程序本身使用的缩写。(这是一个意义深远的想法)- 本
.bss
节适用于可写数据。
2.2.2. 伪指令
伪指令不是 x86/x64 机器的真实指令,伪指令是用于给编译器指示如何进行编译。
2.2.2.1. nasm 定义的 7 种数据 size
- byte : 8 位
- word : 16 位
- dword : 32 位
- qword : 64 位
- tword : 80 位
- oword : 128 位
- yword : 256 位
oword 可以对应 Microsoft MASM 的 xmmword 类型,yword 对应 Microsoft MASM 的 ymmword 类型。
tword, oword 以及 yword 使用在 非整型 数据,使用在 float 和 SSE 型数据。
2.2.2.2. 定义初始化数据:db 家族
nasm 定义了用于初始化上面 7 种 size 的 db 家族,它们用于定义初化常量值。
- db : define byte
- dw :define word
- dd :define doubleword
- dq :define quadword
- dt :define tword
- do :define oword
- dy :define yword
正如前面所说的:dt , do , dy 不接受整型数值常量,它们被使用在定义 float 或 SSE 数据常量。dt 可以定义 extended-precision float 数据,do 可以定义 quad-precision float,dy 可定义 ymm 数据。而 dq 可以定义 double-precision float 数据,dd 可以定义 single-precision float 数据。
2.2.2.3. 定义非初始化数据:resb 家族
程序中使用到的非初始化数据通常放在 bss section
里,bss
代表 uninitialized storage space(如果您试图在一个.text
小节中使用它们,将会出现错误):
nasm 使用了 resb (reserve byte) 家族来定义非初始化数据。
- resb :reserve byte
- resw :reserve word
- resd :reserve doubword
- resq :reserve quadword
- rest :reserve tword
- reso :reserve oword
- resy :reserve yword
resb 相当于 Microsoft MASM 语法中的 db ?
下面是 NASM Manual 的例子:
buffer: resb 64 ; reserve 64 bytes |
2.3.5 使用 equ 定义常量
equ 用来为标识符定义一个 整型 常量,它的作用类似 C 语言中的 #define
a equ 0 ; OK |
例子中: b 定义为常量 ‘abcd’ 它将是字符串的 ASCII 码序列,‘abcdefghi’ 常量将会被截断,整型常量最长为 quadword(8 bytes),而 d 企图被定义为一个 float 常量,这产生会错误。len 和 textlen 被定义为编译期确定的数值。
2.3. 寄存器
您可以将每个寄存器的最低 32 位视为寄存器本身,但可以使用以下名称:
R0D R1D R2D R3D R4D R5D R6D R7D R8D R9D R10D R11D R12D R13D R14D R15D |
您可以使用以下名称将每个寄存器的最低 16 位看作一个寄存器:
R0W R1W R2W R3W R4W R5W R6W R7W R8W R9W R10W R11W R12W R13W R14W R15W |
您可以使用以下名称将每个寄存器的最低 8 位看作一个寄存器:
R0B R1B R2B R3B R4B R5B R6B R7B R8B R9B R10B R11B R12B R13B R14B R15B |
由于历史原因,R0...R3
的第 15 至 8 位被命名为:
AH CH DH BH |
2.4. 操作数
2.4.1. 内存操作数
- 其实就是几种寻址的方式
- 直接寻址
- 寄存器间接寻址
- 寄存器相对寻址
- 基址加变址
- 相对基址加变址
- 注意:没有立即寻址和寄存器寻址
这些是寻址的基本形式:
[ number ]
[ reg ]
[ reg + reg*scale ]
小数位数只能是 1、2、4 或 8[ reg + number ]
[ reg + reg*scale + number ]
这个数字叫做位移 ; 普通寄存器称为基 ; 带有刻度的寄存器称为索引。
例子:
[750] ; 仅位移 |
2.4.2. 直接操作数
这些可以用多种方式编写。以下是官方文档中的一些示例。
200 ; 十进制数 |
2.5. 使用 C 库
仅使用 syscall 编写独立程序就已经很酷了,但很少见。我们想使用 C 库中的好东西。
为何在 C 语言程序中,看上去都是从 main
函数开始执行?这是因为 C library 的内部有_start
标签!_start
开始处的代码会做一些初始化的工作,然后调用main
函数中的代码,最后执行清理工作,最终执行 60 号系统调用以退出。因此,您只需要实现main
函数即可,我们可以在汇编语言中实现这么做:
如果您有 Linux,请尝试以下操作:
hola.asm
; ---------------------------------------------------------------------------------------- |
$ nasm -felf64 hola.asm && gcc hola.o && ./a.out |
2.6. 字符常量
在 nasm 中,可以使用 3 种引号来提供字符
- ’ …’ (单引号)
- ” …” (双引号)
…
(反引号)
如下示例,它们的结果是一样的:
db 'abcd' |
3. 实践 - OS lab1
- 使用 nasm 实现大数加法和乘法
3.1. 思路
输入输出
- 需要熟悉 Linux 系统调用(ics 教过)
- 需要熟悉寄存器的使用
- 打印 int 数组需要 itoa
加法
- 按位加,细节略
减法
- 找出绝对值较大者,计算
dest - src // (abs(dest) > abs(src))
- 按位减,每次减要考虑借位,其他细节略
- 找出绝对值较大者,计算
乘法
$$
基本原理:
\Sigma_{i+j=k}(a_i \times b_j) = c_k
$$先按位乘,最后再统一 normalize
技巧
- string 转 int 数组后倒序存储,方便 int 数组正序遍历
- 比如
"123"
,存储为[3, 2, 1]
3.2. 踩坑
- jmp 的函数无须 ret!!!
- 否则即使 push 的已全部 pop,ret 仍会 segmentation fault
- 调用栈的使用
- ebp 用 leave 弹出
- intel 语法(nasm)的 mov 不接受两个 opcode
- .bss 和.data 定义出的是指针
- 区分字符(’1’)和数字(0x1)
- loop 循环不可轻易 jmp 到压栈代码段(除非是设计好的)
- gdb 和 objdump 结合的使用
- 每次循环后要清空bss和寄存器
4. 参考资料
zhangjunlei26/NASM-Tutorial-CN: Nasm 指南中文 (NASM Tutorial) (github.com)
NASM 与 GDB 的使用指南:如何编好你的汇编 - 知乎 (zhihu.com)
(29 条消息) nasm 汇编讲解_jadeshu 的博客-CSDN 博客_nasm
[学习 nasm 语言-阿里云开发者社区 (aliyun.com)](https://developer.aliyun.com/article/25221#:~:text=代码中使用 byte 关键字对 memory 操作数进行了修饰,指明 memory 操作数的大小为 byte,语法)有些不同, masm 的语法是: 在 masm 语法中需配合 ptr 指示字。)
(29 条消息) GDB 调试查看内存数据_夜风~的博客-CSDN 博客_gdb 查看内存数据