Pengzna's blog 👋

Nov 25, 2022

NASM 概念与实战/踩坑记录

  • NJU OS lab-1
image-20221006001450200

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 -m elf_i386 big_decimal.o -o big_decimal

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
ld -m elf_i386 big_decimal.o -o big_decimal
sudo gdb big_decimal

接下来进入 gdb 界面,开始你的调试工作。

和前面说到的一样,使用gdb 打开你的文件就好了,因为gdb要控制另一个进程,所以别忘了给它开权限。

可以在汇编文件中设置断点,并在你想停止的地方call它,比如

break_demo:
ret

然后只要在gdb

b break_demo

就可以让它在执行到b的时候中止程序。

可以使用指令

i registers

查看代码运行的位置和寄存器状态。

如果你的代码有stdout的输出,可能会破坏这个 layout 的格局,这时候使用refresh指令刷新它。

最后,可以使用x指令查看你的内存状况,比如查看0x40201c开始的 20 个 bits 的内存

x/20b 0x40201c

2. NASM 详解

2.1. NASM 程序的结构

NASM 是基于行的。大多数程序由指令后跟一个或多个部分组成。行可以具有可选标签。大多数行都有一条指令,后跟零个或多个操作数。

image-20221004164300281

通常,您将代码放在的部分中,.text并将常量数据放在的部分中.data

2.2. NASM 语法

2.2.1. 基础指令

有数百条指令。您无法一次全部学习它们。从这些 start:

mov x,y xy
and x,y xx and y
or x,y xx or y
xor x,y xx xor y
add x,y xx + y
sub x,y xxy
inc x xx + 1
dec x xx – 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
wordvar: resw 1 ; reserve a word
realarray resq 10 ; array of ten reals
ymmval: resy 1 ; one YMM register

2.3.5 使用 equ 定义常量

equ 用来为标识符定义一个 整型 常量,它的作用类似 C 语言中的 #define

a  equ 0                          ; OK
b equ 'abcd' ; OK! b = 0x64636261
c equ 'abcdefghi' ; warning! c = 0x6867666564636261
d equ 1.2 ; error!

section .data
string db 'hello,word',0
len equ $-string ; OK! len = 0x0b

section .text
textlen equ _end - entry ; OK! textlen = 0x05

_entry:
mov ecx, textlen

_end:

例子中: 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
EAX ECX EDX EBX ESP EBP ESI EDI

您可以使用以下名称将每个寄存器的最低 16 位看作一个寄存器:

R0W R1W R2W R3W R4W R5W R6W R7W R8W R9W R10W R11W R12W R13W R14W R15W
AX CX DX BX SP BP SI DI

您可以使用以下名称将每个寄存器的最低 8 位看作一个寄存器:

R0B R1B R2B R3B R4B R5B R6B R7B R8B R9B R10B R11B R12B R13B R14B R15B
AL CL DL BL SPL BPL SIL DIL

由于历史原因,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]               ; 仅位移
[rbp] ; 仅基址寄存器
[rcx + rsi*4] ; 基数+指数*比例
[rbp + rdx] ; scale is 1
[rbx-8] ; 位移-8
[rax + rdi*8 + 500] ; 所有四个组成部分
[rbx + counter] ; 使用变量"counter"地址作为偏移

2.4.2. 直接操作数

这些可以用多种方式编写。以下是官方文档中的一些示例。

200         ; 十进制数
0200 ; 仍然是十进制-前导0不会使其变为八进制
0200d ; 显式十进制-d后缀
0d200 ; 也十进制-0d prefex
0c8h ; 十六进制-h后缀,但是前导0是必需的,因为c8h看起来像var
0xc8 ; hex-经典的0x前缀
0hc8 ; 十六进制-由于某些原因,NASM偏爱0h写法
310q ; 八进制-q后缀
0q310 ; 八进制-0q前缀
11001000b ; 二进制-b后缀
0b1100_1000 ; 二进制-0b前缀,顺便说一下,允许使用下划线

2.5. 使用 C 库

仅使用 syscall 编写独立程序就已经很酷了,但很少见。我们想使用 C 库中的好东西。

为何在 C 语言程序中,看上去都是从 main函数开始执行?这是因为 C library 的内部有_start标签!_start开始处的代码会做一些初始化的工作,然后调用main函数中的代码,最后执行清理工作,最终执行 60 号系统调用以退出。因此,您只需要实现main函数即可,我们可以在汇编语言中实现这么做:

如果您有 Linux,请尝试以下操作:

hola.asm

; ----------------------------------------------------------------------------------------
; 使用C库将" Hola,mundo"写入控制台。程序运行在 Linux 或者其他在 C 语言库中不使用下划线的操作系统上。
; 如何编译执行:
; nasm -felf64 hola.asm && gcc hola.o && ./a.out
; ----------------------------------------------------------------------------------------
global main
extern puts

section .text
main: ; 这里被 C library初始化代码所调用
mov rdi, message ; rdi中的第一个整数(或指针)参数
call puts ; puts(message)
ret ; 由 main 函数返回 C 语言库例程
message:
db "Hola, mundo", 0 ; 注意字符串必须在C中以0结尾
$ nasm -felf64 hola.asm && gcc hola.o && ./a.out
Hola, mundo

2.6. 字符常量

在 nasm 中,可以使用 3 种引号来提供字符

  • ’ …’ (单引号)
  • ” …” (双引号)
  • (反引号)

如下示例,它们的结果是一样的:

db  'abcd'
db "abcd"
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 指示字。)

GDB 使用详解 - 知乎 (zhihu.com)

(29 条消息) GDB 调试查看内存数据_夜风~的博客-CSDN 博客_gdb 查看内存数据

(29 条消息) 使用 GDB 查看和修改寄存器的值_@HDS 的博客-CSDN 博客_gdb 查看寄存器的值

Nasm 系统调用 - 字节流 (bytekits.com)

OLDER > < NEWER