# nasm-study **Repository Path**: egu0/nasm-study ## Basic Information - **Project Name**: nasm-study - **Description**: NASM 学习,课程链接:https://www.bilibili.com/video/BV1kH4y1S7cS/ - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2024-05-16 - **Last Updated**: 2024-08-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # NASM Study 本项目是 32 位汇编 和 nasm 学习笔记。教程:[v](https://www.bilibili.com/video/BV1kH4y1S7cS) 汇编器:[NASM](https://nasm.us/) IDE:[SASM 编辑器](https://dman95.github.io/SASM/english.html) SASM 编辑器项目中自带的测试样例:见 `Samples/` 目录 ## 01 寄存器 `x86`平台下的寄存器是 32 位的,分类有: - 四个通用寄存器:`eax, ebx, ecx, edx` - 两个指针寄存器:`esp, ebp` - 其他:`esi, edi` > eax 的低 16 位可以用 ax 表示;ax 的高 8 位可以用 ah 表示、低 8 位可以用 al 表示 示例:基本程序 ```assembly section .text global main main: xor eax, eax;一个数字与自身异或得到 0,是一种清除寄存器数据的方法,0 表示正常 ret ``` 示例:Helloworld ```assembly %include "io.inc" ;引入 SASM 编辑器自带的库,该库在 include 目录中 section .text global main main: PRINT_STRING "hello world!" ;打印字符串 NEWLINE ;换行 PRINT_STRING "how are you?" ;打印字符串 xor eax, eax ret ``` ## 02 算数运算 ### add/sub 加法示例 ```assembly %include "io.inc" section .text global main main: GET_DEC 4, eax ;向 eax 中输入 4 字节数据 GET_DEC 4, ebx mov ecx, eax ; ecx = eax add ecx, ebx ; ecx = ecx + ebx PRINT_DEC 4, ecx ;输出 ecx xor eax, eax ret ``` 减法操作同理:`sub ecx, ebx ; ecx = ecx - ebx` 扩展: - `PRINT_UDEC 4, ecx` 表示把 ecx 看作无符号整数进行打印 - `GET_UDEC 4, ecx` 表示将输入的 4 字节数字看作无符号整数 ### imul 乘法示例(三种写法) ```assembly %include "io.inc" section .text global main main: GET_DEC 4, eax GET_DEC 4, ebx ;imul ebx ; 乘 eax,并将结果放在 eax,即 eax = eax * ebx ;imul ebx, eax ; ebx = ebx * eax imul ebx, eax, 3 ; ebx = eax * 3 PRINT_DEC 4, ebx ;输出 xor eax, eax ret ``` ### idiv 除法示例 ```assembly %include "io.inc" section .text global main main: GET_DEC 4, eax GET_DEC 4, ebx cdq ; 必要的扩展语句,通常放在 idiv 语句前 idiv ebx ; 指定除数。eax = eax / ebx, edx = eax % ebx PRINT_DEC 4, eax ; 除数 NEWLINE PRINT_DEC 4, edx ; 余数 xor eax, eax ret ``` ## 03 逻辑运算 ### xchg 用于交换两个寄存器中的数据 ```assembly %include "io.inc" section .text global main main: GET_DEC 4, eax GET_DEC 4, ebx xchg eax, ebx ;交换数据 PRINT_DEC 4, eax NEWLINE PRINT_DEC 4, ebx xor eax, eax ret ``` ### inc/dec 自增一与自减一 ```assembly inc eax dec ebx ``` ### and ```assembly and eax, ebx ; eax = eax & ebx,寄存器对应位相与 ``` ### or ```assembly or eax, ebx ; eax = eax | ebx,寄存器对应位相或 ``` ### not ```assembly mov eax, 1 ; 0001 not eax ; 1110 ``` ### xor ``` xor eax, eax ; eax = 亦或 ``` ## 04 移位 ### sal/sar 算数左移 `sal` ```assembly sal eax, 1 ; eax 左移 1 位 ``` 示例 ```assembly %include "io.inc" section .text global main main: GET_DEC 4, eax sal eax, 1 ; 左移 1 位 PRINT_DEC 4, eax xor eax, eax ret ``` 注意:`sal` 左移的位数只取给出数字的后 5 位。比如,如果指令是 `sal eax, 32`,因为 32 为 `100000`,它的后五位结果是 0,相当于没有移动。如果要移动 32 位,那么需要用两次左移 16 位替代。 算数右移 `sar` 同理 ### rol/ror 循环左移 `rol` ``` rol eax, 16 ``` 示例 ```assembly %include "io.inc" section .text global main main: GET_DEC 4, eax rol eax, 16 PRINT_DEC 4, eax NEWLINE rol eax, 16 PRINT_DEC 4, eax xor eax, eax ret ``` 循环右移 `ror` 同理 ## 05 转移 ### jmp 无条件转移 ```assembly %include "io.inc" section .text global main main: GET_DEC 4, eax GET_DEC 4, ebx jmp end ;无条件转移 PRINT_DEC 4, eax ;不会执行该行 end: PRINT_DEC 4, ebx xor eax, eax ret ``` ### cmp `cmp` 指令用于比较两个数,比较的结果会被**跳转指令**使用 ```assembly cmp 寄存器, 立即数/寄存器 ``` 示例:较大的数 ```assembly %include "io.inc" section .text global main main: GET_DEC 4, eax GET_DEC 4, ebx cmp eax, ebx jg end ;如果第一个数大于第二个数,那么进行转移 PRINT_DEC 4, ebx ; eax <= ebx xor eax, eax ret end: PRINT_DEC 4, eax ; eax > ebx xor eax, eax ret ``` 其他条件跳转指令及逻辑: - `je` 等于则转移 - `jl` 小于则转移 - `jge` 大于等于零则转移 - `jle` 小于等于零则转移 - `jz` 等于零则转移 ```assembly %include "io.inc" section .text global main main: GET_DEC 4, eax GET_DEC 4, ebx cmp eax, ebx ;jg end ; if (n1 > n2), then jump ;je end ; if (n1 = n2), then jump ;jz end ; if (n1 - n2) > 0 then jump ;jl end ; if (n1 < n2), then jump ;jle end ;if(n1 <= n2), then jump jge end ;if(n1 >= n2), then jump PRINT_DEC 4, ebx xor eax, eax ret end: PRINT_DEC 4, eax xor eax, eax ret ``` ## 06 循环 `loop` 指令的执行:判断 `ecx`,如果不为零,会对 ecx 减一,并跳转到指定的 label ;如果为零则跳过 示例 1 ```assembly %include "io.inc" section .text global main main: mov ecx, 5 cycle: PRINT_DEC 4, ecx ;; The LOOP instruction can't jump to a distance of more than 127 bytes. ;; https://stackoverflow.com/a/12147872/23681037 loop cycle xor eax, eax ret ;; 输出: 54321 ``` 尝试:如果把 `ecx` 初始化为负数或零会发生什么? 示例 2 ```assembly %include "io.inc" section .text global main main: mov eax, 0 mov ecx, 5 loop loop_do_sth loop_do_sth: inc eax ; eax++ ;; 循环结束条件 cmp eax, 3 jg end_loop ; if eax > 3, then end loop ;; 如果不满足跳出条件则继续执行 loop loop_do_sth end_loop: PRINT_DEC 4, eax; 4 NEWLINE PRINT_DEC 4, ecx; 1 xor eax, eax ret ``` ## 07 数组 示例:数组基础操作 ```assembly %include "io.inc" section .bss N equ 5 ; 定义常量 arr resd N ; 通过 resd 指令定义数组,为它分配 N 个 dword(一共 N*4 字节)。注意:arr 是自定义的数组名,可自定义 section .text global main main: ;; 1.读取数组 ;; ecx = N, ebx = 0 mov ecx, N xor ebx, ebx cycle_fill: ;; arr[ebx++] = read_num() GET_DEC 4, [arr + 4 * ebx] inc ebx ;; while (ecx-- > 0) loop cycle_fill ;; 2.打印数组 ;; ecx = N, ebx = 0 mov ecx, N xor ebx, ebx cycle_print: ;; print(arr[ebx++]) PRINT_DEC 4, [arr + 4 * ebx] inc ebx ;; while (ecx-- > 0) loop cycle_print xor eax, eax ret ``` ![image-20240521140244556](./README.assets/image-20240521140244556.png) 注意: - 通过 `[arr + 偏移量]` 访问数组内的元素,偏移量的计算公式为 `offset = 4 * index`,index 从 0 开始;数组中的元素是 dword 类型,占 4 byte(byte=1B、word=2B、dword=4B、qword=8B) - 推荐使用 ebx 作为偏移量访问数组中元素,因为它是基地址寄存器,比如 `[arr + 8 * ebx]`。[参考](https://blog.csdn.net/zhu2695/article/details/16813425) **示例 2**:计算平均数 ```assembly %include "io.inc" section .bss N equ 5 arr resd N ; reserve N dword, total N*4 Bytes section .text global main main: ;; 1.读取数组 ;; ecx = N, ebx = 0 mov ecx, N xor ebx, ebx cycle_fill: ;; arr[ebx++] = read_num() GET_DEC 4, [arr + 4 * ebx] inc ebx ;; while (ecx-- > 0) loop cycle_fill ;; 2.累加 ;; ecx = N, ebx = 0 mov ecx, N xor ebx, ebx xor eax, eax cycle_print: ;; eax = eax + arr[ebx++] add eax, [arr + 4 * ebx] inc ebx ;; while (ecx-- > 0) loop cycle_print ;; 3.除法,得到商和余数 cdq idiv ebx ; eax = eax / ebx PRINT_DEC 4, eax ; division NEWLINE PRINT_DEC 4, edx ; mod xor eax, eax ret ``` ## 08 栈的使用 栈用来存放临时变量,其中栈顶指针 SP 指向栈顶元素 可以通过 `[esp + offset]` 访问栈内元素,`[esp + offset]` 表示偏移 offset 个字节进行访问,通常 offset 是 4 或 8 的整数倍,即 `offset = 4N` 或 `offset = 8N` > 对于 `x86` 平台,sp 实际上是 `esp`,栈内元素大小为 DWORD > > 对于 `x64` 平台,sp 实际上是 `rsp`,栈内元素大小为 QWORD 示意图 ![image-20240521144111278](./README.assets/image-20240521144111278.png) 注意:**程序退出前,需要清空栈内元素**。示例 ```assembly %include "io.inc" section .text global main main: push 10 ;程序结束前,需要清空栈内元素,即运行过程中加入的元素,有以下两种方法 ;1. 修改 esp,添加 4N,其中 N 为新添加的元素个数 ;add esp, 4 ;2. pop 弹出元素,可以得到其中的元素,会自动修改 esp 的值 ;pop rax xor eax, eax ret ``` 补充:esp 用于记录栈顶指针,`ebp` 用来记录栈底指针 ## 09 函数调用 内容:调用函数,并通过栈传递参数 指令: - `call` 调用函数,会将下一条指令的地址压栈 - `ret` 函数返回,会将栈顶元素弹栈并作为目标跳转地址 寄存器: - `esp` 栈顶指针 - `ebp` 栈低指针 示例:调用函数计算两个数的平均数 ```assembly %include "io.inc" section .text global main main: push 10 push 20 call avg_fun ; call 命令会将下一条指令的地址压栈后进行跳转 add esp, 8 ; 修改 esp,恢复空栈。也可以两次 pop PRINT_DEC 4, eax xor eax, eax ret avg_fun: xor eax, eax ; eax = 0 add eax, [esp + 4] ; eax += 10 add eax, [esp + 8] ; eax += 20 sar eax, 1 ; 右移 1 位 ret ; 此时栈顶元素记录的是要跳回的位置,执行 ret 会弹栈并跳回 ``` **更加规范的写法** ⭐ ```assembly %include "io.inc" section .text global main main: mov ebp, esp; 程序开始时,修改 ebp = esp,表示空栈,有利于之后的调试 ;调用函数,通过栈传递参数 push 10 push 20 call avg_fun ; call 命令会将下一条指令的地址压栈后进行跳转 add esp, 8 ; 修改 esp,恢复空栈。也可以两次 pop,也可以 mov esp, ebp PRINT_DEC 4, eax xor eax, eax ret avg_fun: push ebp ; 保存 ebp,即旧的栈低指针 mov ebp, esp ; 新的作用域开始时使 ebp = esp,方便调试 xor eax, eax ; eax = 0 add eax, [ebp + 8] ; eax += 10。推荐使用 ebp 计算偏移量,因为它通常不会改变。使用 esp 也没有错 add eax, [ebp + 12] ; eax += 20 sar eax, 1 ; 右移 1 位 pop ebp ; 恢复 ebp ret ; 此时栈顶元素记录的是要跳回的位置,执行 ret 会弹栈并跳回 ``` ## 10 宏定义 示例:定义宏 IFXNEY,用来比较两个数是否相等,如果不相等则跳转到指定 label ```assembly %include "io.inc" ;; 定义宏 IFXNEY,用来比较两个数是否相等,如果不相等则跳转到指定 label %MACRO IFXNEY 3 mov eax, dword [%1] cmp eax, dword [%2] jne %3 ;; jump to %3 if eax (essential [%1]) is not equal to [%2] %ENDMACRO ;; 定义常量 ;; more at https://ncona.com/2019/02/assembly-variables-instructions-and-addressing-modes/ section .data X dd 15 ;; 创建 4 字节常量 X,值为 10 Y dd 15 ;; dw, dd, dq section .text global main main: ;; 使用宏 IFXNEY X, Y, M1 PRINT_STRING "equal" jmp M2 M1: PRINT_STRING "not equal" M2: xor eax, eax ret ``` 示例:定义宏计算绝对值 ```assembly %include "io.inc" %MACRO CALABS 1 mov eax, dword [%1] mov ebx, 0 cmp eax, ebx jl %%_abs_hit ;; 给 label 添加特殊前缀 %%_ 以区分宏外部的 label jmp %%_abs_end %%_abs_hit: ;; neg 指令作用是计算负值 neg eax %%_abs_end: ;; push result into stack push eax %ENDMACRO section .data X dd -10 section .text global main main: CALABS X pop eax PRINT_DEC 4, eax xor eax, eax ret ``` 宏的形参比较灵活,可以是地址、寄存器、数组、标签等 ```assembly %include "io.inc" %MACRO TEST 4 mov %1, %3 mov ebx, [%2] add %1, ebx jmp %4 %ENDMACRO section .data X dd 5 section .text global main main: TEST eax, X, 10, print_eax PRINT_STRING "this line will not appear in output" print_eax: PRINT_DEC 4, eax xor eax, eax ret ``` ## 11 汇总 **标志位** - `OF` 是否溢出 - `SF` 是否为正 - `ZF` 是否为零 - `PF` 是否有偶数个 1 - `CF` 是否有进位 - `AF` 后四位是否有进位 [参考](https://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture#EFLAGS_Register) **外部 io.inc 库常用函数** - `PRINT_STRING ""` - `PRINT_DEC 4, eax`、`PRINT_UDEC 4, eax`、`PRINT_HEX 4, eax` - `GET_DEC 4, eax` **移动和交换数据** - `mov` - `movsx` - `movzx` - `xchg` **栈操作** - `push`、`pop` - `pusha`、`popa` - `pushad`、`popad` **转换** - `cbw` - `cwd` - `cwde` - `cdq` **算数** - `add`、`adc` - `sub`、`sbb` - `mul`、`imul` - `div`、`idiv` **逻辑** - `not` - `and` - `or` - `xor` **移位** - `sal`、`sar` - `shl`、`shr` - `ror`、`rol` - `rcr`、`rcl` - `shld`、`shrd` **循环和跳转** - `jmp` - `jcc` - `jg`、`jl`、`jge`、、 - `loop` - `loope`、`loopz` - `loopne`、`loopnz` **其他** - `lea eax, 1` 表示将数据 1 的有效地址传递给 eax - `neg` - `cmp` 比较两个寄存器,会改变一些符号位 - `inc`、`dec` ## 12 使用 gdb 调试工具 示例程序 ```assembly section .data n1 db 10 ;; 1 bytes n2 db 20 section .text global _start _start: mov ecx, [n2] ; 默认会读取 4 字节 mov ebx, [n1] ; exit code mov eax, 1 ; exit interrupt int 80h ``` 编译 ```sh $ nasm -f elf32 out.asm $ ld -m efl_i386 -o out out.o ``` 使用 gdb 工具进行调试,所用到的命令: ``` $ gdb out ##### 呈现 asm 布局? (gdb) layout asm ##### 打断点 (gdb) break _start ##### 开始调试 (gdb) run ##### 执行下一条指令 (gdb) stepi ##### 查看所有寄存器 (gdb) info registers ##### 查看指定寄存器 (gdb) info registers ebx ##### 查看指定内存地址的数据 (gdb) x/x 0x123456 ``` **问题**:在单步调试时发现 ebx 并不是 10,而是 5130 ![image-20240522133901530](./README.assets/image-20240522133901530.png) **原因**:在数据段中定义的数据长度是 1 字节,但是读取时一次性读了 4 字节,读取的内容是 `0x0000140a`,最低的字节是 `0a` 表示 10,倒数第二低字节是 `14` 表示 20。通过这里也可以看出数据段中的定义的两个变量是连续存储的并且每个占用一字节 **解决**: - 方法一:定义逻辑。修改数据段中使用的指令为 `dd`(db 表示一字节、dw 表示两字节、dd 表示四字节、dq 表示八字节) - 方法二:读取逻辑。使用 `movzx`/`movsx` 指令,[参考](https://stackoverflow.com/a/20727400/23681037) - 方法三:读取逻辑。只修改 `ebx` 的最低一字节,即 `mov bl, [n1]`(ebx 是四字节,bx 是低位的两字节、bh 是倒数第二字节、bl 是倒数第一字节) 本节[教程](https://www.youtube.com/watch?v=LIM-PtQprFQ&list=PL2EF13wm-hWCoj6tUBGUmrkJmH1972dBB&index=9)