├── assets ├── rdx.png ├── nasm-logo.png └── nasmstructure.png ├── x86-64-psABI-1.0.pdf ├── calling_conventions.pdf └── README.md /assets/rdx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjunlei26/NASM-Tutorial-CN/HEAD/assets/rdx.png -------------------------------------------------------------------------------- /assets/nasm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjunlei26/NASM-Tutorial-CN/HEAD/assets/nasm-logo.png -------------------------------------------------------------------------------- /x86-64-psABI-1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjunlei26/NASM-Tutorial-CN/HEAD/x86-64-psABI-1.0.pdf -------------------------------------------------------------------------------- /calling_conventions.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjunlei26/NASM-Tutorial-CN/HEAD/calling_conventions.pdf -------------------------------------------------------------------------------- /assets/nasmstructure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangjunlei26/NASM-Tutorial-CN/HEAD/assets/nasmstructure.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nasm指南中文 (NASM Tutorial) 2 | 3 | 4 | 5 | 英文原文链接:http://cs.lmu.edu/~ray/notes/nasmtutorial/ 6 | 7 | > NASM 是一个绝赞的汇编器。现在让我们通过一些例子来学习 NASM。 然而这里的笔记仅仅只是蜻蜓点水般地涉及了一些皮毛,所以当你看完这个页面后,你需要查看 [官方的 NASM 文档 ](http://www.nasm.us/doc/)。 8 | > 9 | > 因原文写的时间应该比较早,翻译该文章是在2020年2月2日,原文中的资源链接已不能再打开,有的示例代码在我的MAC OSX Catalina 10.15.3上也无法正常编译通过,笔者针对上述问题进行了修正,但编译问题仅在MAC OSX下验证可以正常编译,Linux下尚未进行验证。 10 | 11 | ![img](https://raw.githubusercontent.com/zhangjunlei26/NASM-Tutorial-CN/master/assets/nasm-logo.png) 12 | 13 | ## 教程范围 14 | 15 | 本教程将向您展示如何在 x86-64 体系结构上编写汇编语言程序。您将同时学会编写: 16 | 17 | 1. 独立运行的汇编程序 18 | 2. 与 C 集成的程序 19 | 20 | > 请注意:教程会涉及Windows/MacOSX/Linux三个系统的使用方法,请确保在其中一个平台下已经安装好 nasm 和 gcc。 21 | 22 | ## 第一个程序 23 | 24 | 在学习 nasm 之前,请确保您可以键入并运行程序。 25 | 26 | 确保同时安装了 nasm 和 gcc。将以下程序之一另存为`hello.asm`,具体取决于您的计算机平台。然后根据给定的说明运行程序。 27 | 28 | 如果您使用的是基于 Linux 的操作系统: 29 | 30 | `hello.asm` 31 | 32 | ```asm 33 | ; ---------------------------------------------------------------------------------------- 34 | ; 仅使用syscall将"Hello,World"写入控制台。仅在64位Linux上运行。 35 | ; 使用 Linux 下的 1 号系统调用来输出一条信息和 60 号系统调用来退出程序。 36 | ; 编译汇编代码并运行: 37 | ; 38 | ; nasm -felf64 hello.asm && ld hello.o && ./a.out 39 | ; ---------------------------------------------------------------------------------------- 40 | 41 | global _start 42 | 43 | section .text 44 | _start: 45 | ; write(1, message, 13) 46 | mov rax,1 ; 1 号系统调用是写操作 47 | mov rdi,1 ; 1 号文件系统调用是标准输出stdout 48 | mov rsi,message ; 输出字符串的地址 49 | mov rdx,13 ; 字节数(输出字符串的长度) 50 | syscall ; 调用操作系统进行写入 51 | 52 | ; exit(0) 53 | mov rax,60 ; 60号系统调用是退出 54 | xor rdi,rdi ; 退出代码 0 55 | syscall ; 调用操作系统退出 56 | 57 | section .data 58 | message: db "Hello,World",10 ; 注意最后的换行符 59 | 60 | ``` 61 | 62 | ```shell 63 | $ nasm -felf64 hello.asm && ld hello.o && ./a.out 64 | Hello,World 65 | ``` 66 | 67 | 68 | 69 | 如果您使用的是 macOS: 70 | 71 | `hello.asm` 72 | 73 | ```asm 74 | ; ---------------------------------------------------------------------------------------- 75 | ; 仅使用syscall将" Hello,World"写入控制台。仅在64位macOS上运行。 76 | ; 77 | ; 显示nasm支持的输出格式 78 | ; nasm -hf 79 | ; 80 | ; 编译汇编代码并运行: 81 | ; nasm -fmacho64 hello.asm && ld -o hello -e _main -lSystem hello.o && ./hello 82 | ; 也可以如下方式运行: 83 | ; nasm -fmacho64 hello.asm && gcc -o hello hello.o && ./hello 84 | ; 85 | ; ---------------------------------------------------------------------------------------- 86 | 87 | section .data 88 | msg: db "Hello, World!",10,0 ; 注意最后的换行符 89 | len: equ $-msg 90 | 91 | 92 | section .text 93 | global _main 94 | 95 | ; kernel: 96 | ; syscall 97 | ; ret 98 | 99 | _main: 100 | ; write(1, msg, 13) 101 | mov rax,0x02000004 ; 系统调用 102 | mov rdi,1 ; 文件句柄号1是stdout 103 | mov rsi,msg ; 要输出的字符串地址 104 | mov rdx,len ; 15字节数 105 | ; exit(0) 106 | syscall ; 调用操作系统进行写入 107 | ; call kernel 108 | 109 | mov rax,0x02000001 ; syscall退出 110 | xor rdi,rdi ; 退出代码0 111 | syscall ; 调用操作系统退出 112 | ; call kernel 113 | 114 | 115 | 116 | ``` 117 | ```shell 118 | $ nasm -fmacho64 hello.asm && gcc hello.o && ./a.out 119 | Hello, World 120 | ``` 121 | > **练习**:确定两个程序之间的差异。 122 | 123 | ## NASM 程序的结构 124 | 125 | NASM 是基于行的。大多数程序由指令后跟一个或多个部分组成。行可以具有可选标签。大多数行都有一条指令,后跟零个或多个操作数。 126 | 127 | ![nasmstructure.png](https://raw.githubusercontent.com/zhangjunlei26/NASM-Tutorial-CN/master/assets/nasmstructure.png) 128 | 129 | 通常,您将代码放在的部分中,`.text`并将常量数据放在的部分中`.data`。 130 | 131 | ## 细节 132 | 133 | NASM 是一个很棒的汇编器,但是汇编语言很复杂。您不仅需要教程。您需要详细信息。很多细节。准备咨询: 134 | 135 | - [NASM 手册](http://www.nasm.us/doc/),非常好! 136 | - [英特尔处理器手册](http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html) 137 | 138 | ## 从掌握如下汇编指令开始 139 | 140 | 有数百条指令。您无法一次全部学习它们。从这些 start: 141 | 142 | | `mov` _x_,_y_ | _x_ ← _y_ | 143 | | ------------- | ---------------------------------------------------------------------------------------------------------------- | 144 | | `and` _x_,_y_ | _x_ ← *x* and *y* | 145 | | `or` _x_,_y_ | _x_ ← *x* or *y* | 146 | | `xor` _x_,_y_ | _x_ ← _x_ xor _y_ | 147 | | `add` _x_,_y_ | _x_ ← _x_ + _y_ | 148 | | `sub` _x_,_y_ | _x_ ← _x_ – _y_ | 149 | | `inc` _x_ | _x_ ← _x_ + 1 | 150 | | `dec` _x_ | _x_ ← _x_ – 1 | 151 | | `syscall` | 调用操作系统例程 | 152 | | `db` | 一个[伪指令](http://www.nasm.us/xdoc/2.11.02/html/nasmdoc3.html#section-3.2) 声明字节,这将是在内存中的程序运行时 | 153 | 154 | ## 三种操作数 155 | 156 | ### 寄存器操作数 157 | 158 | 在本教程中,我们只关心整数寄存器和 xmm 寄存器。您应该已经知道什么是寄存器,但是这里是一个快速的回顾。16 个整数寄存器为 64 位宽,称为: 159 | 160 | ```asm 161 | R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 R11 R12 R13 R14 R15 162 | RAX RCX RDX RBX RSP RBP RSI RDI 163 | ``` 164 | 165 | (请注意,其中的最后 8 个寄存器具有备用名称)您可以将每个寄存器的最低 32 位视为寄存器本身,但可以使用以下名称: 166 | 167 | ```asm 168 | R0D R1D R2D R3D R4D R5D R6D R7D R8D R9D R10D R11D R12D R13D R14D R15D 169 | EAX ECX EDX EBX ESP EBP ESI EDI 170 | ``` 171 | 172 | 您可以使用以下名称将每个寄存器的最低 16 位看作一个寄存器: 173 | 174 | ```asm 175 | R0W R1W R2W R3W R4W R5W R6W R7W R8W R9W R10W R11W R12W R13W R14W R15W 176 | AX CX DX BX SP BP SI DI 177 | ``` 178 | 179 | 您可以使用以下名称将每个寄存器的最低 8 位看作一个寄存器: 180 | 181 | ```asm 182 | R0B R1B R2B R3B R4B R5B R6B R7B R8B R9B R10B R11B R12B R13B R14B R15B 183 | AL CL DL BL SPL BPL SIL DIL 184 | ``` 185 | 186 | 由于历史原因,`R0...R3`的第 15 至 8 位被命名为: 187 | 188 | ``` 189 | AH CH DH BH 190 | ``` 191 | 192 | 最后,有 16 个 XMM 寄存器,每个 128 位宽,名为: 193 | 194 | ``` 195 | XMM0 ... XMM15 196 | ``` 197 | 198 | 研究这张照片;希望它可以帮助: 199 | 200 | ![rdx.png](https://raw.githubusercontent.com/zhangjunlei26/NASM-Tutorial-CN/master/assets/rdx.png) 201 | 202 | ### 内存操作数 203 | 204 | 这些是寻址的基本形式: 205 | 206 | - `[ number ]` 207 | - `[ reg ]` 208 | - `[ reg + reg*scale ]` _小数位数只能是 1、2、4 或 8_ 209 | - `[ reg + number ]` 210 | - `[ reg + reg*scale + number ]` 211 | 212 | 这个数字叫做**位移** ; 普通寄存器称为**基** ; 带有刻度的寄存器称为**索引**。 213 | 214 | 例子: 215 | 216 | ```asm 217 | [750] ; 仅位移 218 | [rbp] ; 仅基址寄存器 219 | [rcx + rsi*4] ; 基数+指数*比例 220 | [rbp + rdx] ; scale is 1 221 | [rbx-8] ; 位移-8 222 | [rax + rdi*8 + 500] ; 所有四个组成部分 223 | [rbx + counter] ; 使用变量"counter"地址作为偏移 224 | ``` 225 | 226 | ### 直接操作数 227 | 228 | 这些可以用多种方式编写。以下是官方文档中的一些示例。 229 | 230 | ```asm 231 | 200 ; 十进制数 232 | 0200 ; 仍然是十进制-前导0不会使其变为八进制 233 | 0200d ; 显式十进制-d后缀 234 | 0d200 ; 也十进制-0d prefex 235 | 0c8h ; 十六进制-h后缀,但是前导0是必需的,因为c8h看起来像var 236 | 0xc8 ; hex-经典的0x前缀 237 | 0hc8 ; 十六进制-由于某些原因,NASM偏爱0h写法 238 | 310q ; 八进制-q后缀 239 | 0q310 ; 八进制-0q前缀 240 | 11001000b ; 二进制-b后缀 241 | 0b1100_1000 ; 二进制-0b前缀,顺便说一下,允许使用下划线 242 | 243 | ``` 244 | 245 | ## 具有两个内存操作数的指令非常少见 246 | 247 | 实际上,在本教程中我们将看不到任何此类说明。大多数基本说明只有以下几种形式: 248 | 249 | | `add` _reg_, _reg_ | 250 | | ------------------ | 251 | | `add` _reg_,_mem_ | 252 | | `add` _reg_, _imm_ | 253 | | `add` _mem_,_reg_ | 254 | | `add` _mem_, _imm_ | 255 | 256 | ## 定义数据并保留空间 257 | 258 | 这些示例来自 [docs 的第 3 章](https://cs.lmu.edu/~ray/notes/nasmtutorial/)。要将数据放入内存中: 259 | 260 | ```asm 261 | db 0x55 ; 只是字节0x55 262 | db 0x55,0x56,0x57 ; 连续三个字节 263 | db 'a',0x55 ; 字符常量可以 264 | db 'hello',13,10,'$' ; 字符串常量也是如此 265 | dw 0x1234 ; 0x34 0x12 266 | dw 'a' ; 0x61 0x00(只是一个数字) 267 | dw 'ab' ; 0x61 0x62(字符常量) 268 | dw 'abc' ; 0x61 0x62 0x63 0x00(字符串) 269 | dd 0x12345678 ; 0x78 0x56 0x34 0x12 270 | dd 1.234567e20 ; 浮点常数 271 | dq 0x123456789abcdef0 ; 八字节常量 272 | dq 1.234567e20 ; 双精度浮点 273 | dt 1.234567e20 ; 扩展精度浮点 274 | 275 | ``` 276 | 277 | 还有其他形式。请稍候自行查阅 NASM 文档。 278 | 279 | 要保留空间(无需初始化),可以使用以下伪指令。它们应该放在一个称为`.bss`的小节中(如果您试图在一个`.text`小节中使用它们,将会出现错误): 280 | 281 | ```asm 282 | buffer: resb 64 ; 保留64个字节 283 | wordvar: resw 1 ; 保留一个字 284 | realarray: resq 10 ; 十个实数的数组 285 | ``` 286 | 287 | ## 另一个例子 288 | 289 | 这是一个要研究的 macOS 程序: 290 | 291 | `triangle.asm` 292 | 293 | ```asm 294 | ; ---------------------------------------------------------------------------------------- 295 | ; 这是一个OSX控制台程序,将星号的小三角形写成标准 296 | ; 输出。仅在macOS上运行。 297 | ; 298 | ; nasm -fmacho64 triangle.asm && gcc hola.o && ./a.out 299 | ; ---------------------------------------------------------------------------------------- 300 | 301 | global _main 302 | default rel 303 | 304 | section .text 305 | _main: 306 | push rbx ; OSX必须,保存栈,Linux下删除该行 307 | mov rdx, output ; rdx holds address of next byte to write 308 | mov r8, 1 ; initial line length 309 | mov r9, 0 ; number of stars written on line so far 310 | line: 311 | mov byte [rdx], '*' ; write single star 312 | inc rdx ; advance pointer to next cell to write 313 | inc r9 ; "count" number so far on line 314 | cmp r9, r8 ; did we reach the number of stars for this line? 315 | jne line ; not yet, keep writing on this line 316 | lineDone: 317 | mov byte [rdx], 10 ; write a new line char 318 | inc rdx ; and move pointer to where next char goes 319 | inc r8 ; next line will be one char longer 320 | mov r9, 0 ; reset count of stars written on this line 321 | cmp r8, maxlines ; wait, did we already finish the last line? 322 | jng line ; if not, begin writing this line 323 | done: 324 | mov rax, 0x02000004 ; system call for write 325 | mov rdi, 1 ; file handle 1 is stdout 326 | mov rsi, output ; address of string to output 327 | mov rdx, dataSize ; number of bytes 328 | syscall ; invoke operating system to do the write 329 | 330 | ;exit(0) 331 | pop rbx ; OSX必须,弹出开头保存的栈,Linux下删除该行 332 | ;mov rax, 0x02000001 ; system call for exit 333 | ;xor rdi, rdi ; exit code 0 334 | ;syscall ; invoke operating system to exit 335 | ret 336 | 337 | section .bss 338 | maxlines equ 8 339 | dataSize equ 44 340 | output: resb dataSize 341 | 342 | 343 | ``` 344 | 345 | ```shell 346 | $ nasm -fmacho64 triangle.asm && ld triangle.o && ./a.out 347 | * 348 | ** 349 | *** 350 | **** 351 | ***** 352 | ****** 353 | ******* 354 | ******** 355 | ``` 356 | 357 | 358 | 359 | 此示例中的新内容: 360 | 361 | - `cmp` 做比较 362 | - `je`如果先前的比较相等则跳转。 363 | - `jne`(如果不等于则跳转) 364 | - `jl`(如果不等于则跳转) 365 | - `jnl`(如果不小于则跳转) 366 | - `jg`(如果大于则跳转) 367 | - `jng`(如果不大于则跳转) 368 | - `jle`(如果小于或等于则跳转) 369 | - `jnle`(如果不小于或等于则跳转) 370 | - `jge`(如果大于或等于则跳转) 371 | - `jnge`(如果不大于或等于则跳转) 372 | - `equ`实际上不是真正的指令。它只是定义了供汇编程序本身使用的缩写。(这是一个意义深远的想法) 373 | - 本`.bss`节适用于*可写*数据。 374 | 375 | ## 使用 C 库 376 | 377 | 仅使用 syscall 编写独立程序就已经很酷了,但很少见。我们想使用 C 库中的好东西。 378 | 379 | 为何在 C语言程序中,看上去都是从 `main`函数开始执行?这是因为 C library的内部有`_start`标签!`_start`开始处的代码会做一些初始化的工作,然后调用`main`函数中的代码,最后执行清理工作,最终执行60号系统调用以退出。因此,您只需要实现`main`函数即可,我们可以在汇编语言中实现这么做: 380 | 381 | 如果您有 Linux,请尝试以下操作: 382 | 383 | hola.asm 384 | 385 | ```asm 386 | ; ---------------------------------------------------------------------------------------- 387 | ; 使用C库将" Hola,mundo"写入控制台。程序运行在 Linux 或者其他在 C 语言库中不使用下划线的操作系统上。 388 | ; 如何编译执行: 389 | ; nasm -felf64 hola.asm && gcc hola.o && ./a.out 390 | ; ---------------------------------------------------------------------------------------- 391 | global main 392 | extern puts 393 | 394 | section .text 395 | main: ; 这里被 C library初始化代码所调用 396 | mov rdi, message ; rdi中的第一个整数(或指针)参数 397 | call puts ; puts(message) 398 | ret ; 由 main 函数返回 C 语言库例程 399 | message: 400 | db "Hola, mundo", 0 ; 注意字符串必须在C中以0结尾 401 | ``` 402 | 403 | ```shell 404 | $ nasm -felf64 hola.asm && gcc hola.o && ./a.out 405 | Hola, mundo 406 | ``` 407 | 408 | 409 | 410 | 在 macOS 下,看起来会有些不同: 411 | 412 | hola.asm 413 | 414 | ```asm 415 | ; ---------------------------------------------------------------------------------------- 416 | ; 这是一个macOS控制台程序,它在一行上写入" Hola,mundo",然后退出。 417 | ; 它使用C库中的puts。编译汇编代码并运行: 418 | ; 419 | ; nasm -fmacho64 hola.asm && gcc hola.o && ./a.out 420 | ; ---------------------------------------------------------------------------------------- 421 | global _main 422 | extern _puts 423 | 424 | section .text 425 | _main: push rbx ; 调用堆栈必须对齐 426 | lea rdi, [rel message] ; 第一个参数是消息的地址 427 | call _puts ; puts(message) 428 | pop rbx ; Fix up stack before returning 429 | ret 430 | 431 | section .data 432 | message: db "Hola, mundo", 0 ; C字符串末尾需要一个零字节结尾 433 | 434 | ``` 435 | 436 | ```shell 437 | $ nasm -fmacho64 hola.asm && gcc hola.o && ./a.out 438 | Hola, mundo 439 | ``` 440 | 441 | 442 | 443 | 在 macOS 领域中,C 函数(或实际上是从一个模块导出到另一个模块的任何函数)必须以下划线作为前缀。调用堆栈必须在 16 字节边界上对齐(稍后会对此进行更多介绍)。并且在访问命名变量时,需要`rel`前缀。 444 | 445 | ## 理解参数调用约束 446 | 447 | 我们怎么知道`puts`的参数放在`RDI`中?答:有多个参数调用约定。 448 | 449 | 当你为 C library集成的 64 位 Linux 编写代码时,必须遵循[《AMD64 ABI Reference》]([here](https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf))中说明的调用约定 。您也可以从[Wikipedia](http://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_Calling_Conventions)获这些信息 。 450 | 451 | > 注:` x86-64 System V ABI`下载地址曾是`http://x86-64.org/documentation/abi.pdf`,现在 System V x86-64 psABI 已迁移到GitHub上维护,最新版本可以查看 [J.J. Lu的Github Wiki](https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI)。 452 | 453 | 在这里列出最重要的几点: 454 | 455 | - 传递参数时,按照从左到右的顺序,将尽可能多的参数依次保存在寄存器中。存放位置的寄存器顺序是确定的: 456 | - 对于整数和指针:`rdi`,`rsi`,`rdx`,`rcx`,`r8`,`r9`。 457 | - 对于浮点(float和double类型):`xmm0`,`xmm1`,`xmm2`,`xmm3`,`xmm4`,`xmm5`, ` xmm6`,`xmm7` 458 | - 剩下的参数将按照从右到左的顺序压入栈中,并在调用之后 *由调用函数推出栈* 。 459 | - 等所有的参数传入后,会生成调用指令。所以当被调用函数得到控制权后,返回地址会被保存在 `[rsp]` 中,第一个局部变量会被保存在 `[rsp+8]` 中,以此类推。 460 | - **栈指针`rsp`在调用之前,必须与16 字节边界对齐处理**。当然,调用的过程中只会把一个 8 bytes 的返回地址推入栈中,所以当函数得到控制权时,`rsp` 并没有对齐。你需要向栈中压入数据或者从 `rsp` 减去 8 来使之对齐。 461 | - 调用函数需要预留如下寄存器(the calle-save registers):`rbp`,`rbx`,`r12`,`r13`,`r14`,`r15`。其他的寄存器可以自由使用。 462 | - 被调用函数也需要保存 XMCSR 的控制位和 x87 指令集的控制字,但是 x87 指令在 64 位系统中很少见,所以您不必担心这一点。 463 | - 整数被返回在`rax`或`rdx:rax`,浮点值返回在`xmm0`或`xmm1:xmm0`。 464 | 465 | 以上罗列的都理解了吗?什么,还没有?没关系,接下来我们再来一些示例,练习一下。 466 | 467 | 如下代码,展示如何保存和恢复寄存器: 468 | 469 | `fib.asm` 470 | 471 | ```asm 472 | ; ---------------------------------------------------------------------------- 473 | ; 一个写入前90个斐波那契数字的64位Linux应用程序。至 474 | ; 编译汇编代码并运行: 475 | ; 476 | ; nasm -felf64 fib.asm && gcc fib.o && ./a.out 477 | ; ---------------------------------------------------------------------------- 478 | 479 | global main 480 | extern printf 481 | 482 | section .text 483 | main: 484 | push rbx ; 我们必须保存它,因为我们使用它 485 | 486 | mov ecx,90 ; ecx将倒数至0 487 | xor rax,rax ; rax将保留当前数字 488 | xor rbx,rbx ; rbx将保留下一个数字 489 | inc rbx ; rbx最初是1 490 | print: 491 | ; 我们需要调用printf,但是我们使用的是rax,rbx和rcx。打印 492 | ; 可能会破坏rax和rcx,因此我们将在调用之前保存它们,并且 493 | ; 之后恢复它们。 494 | 495 | push rax ; caller-save register 496 | push rcx ; caller-save register 497 | 498 | mov rdi,printf ; 设置第一个参数(format) 499 | mov rsi,rax ; 设置第二个参数(current_number) 500 | xor rax,rax ; 因为printf是varargs 501 | 502 | ; 堆栈已经对齐,因为我们压入了三个8字节寄存器 503 | call printf ; printf(format, current_number) 504 | 505 | pop rcx ; restore caller-save register 506 | pop rax ; restore caller-save register 507 | 508 | mov rdx,rax ; save the current number 509 | mov rax,rbx ; next number is now current 510 | add rbx,rdx ; get the new next number 511 | dec ecx ; count down 512 | jnz print ; if not done counting, do some more 513 | 514 | pop rbx ; returing之前还原rbx 515 | ret 516 | format: 517 | db "%20ld",10,0 518 | 519 | ``` 520 | 521 | ```shell 522 | $ nasm -felf64 fib.asm && gcc fib.o && ./a.out 523 | 0 524 | 1 525 | 1 526 | 2 527 | . 528 | . 529 | . 530 | 679891637638612258 531 | 1100087778366101931 532 | 1779979416004714189 533 | ``` 534 | 535 | 536 | 537 | 通过如上代码,我们学习如下指令: 538 | 539 | | `push` _x_ | 减少`rsp`操作数的大小,然后将*x*存储在`[rsp]` | 540 | | -------------- | -------------------------------------------- | 541 | | `pop` _x_ | 移动`[rsp]`到*X*,然后增加`rsp`由操作数的大小 | 542 | | `jnz` _label_ | 如果设置了处理器的 Z(零)标志,请跳至给定标签 | 543 | | `call` _label_ | 按下下一条指令的地址,然后跳到标签 | 544 | | `ret` | 弹出指令指针 | 545 | 546 | ## C 和汇编语言混合调用示例 547 | 548 | 该程序只是一个简单的函数,它接受三个整数参数并返回最大值。 549 | 550 | `maxofthree.asm` 551 | 552 | ```asm 553 | ; -------------------------------------------------- --------------------------- 554 | ; 一个64位函数,该函数返回其三个64位整数的最大值 555 | ; 论点。该函数具有签名: 556 | ; 557 | ; int64_t maxofthree(int64_t x,int64_t y,int64_t z) 558 | ; 559 | ; 请注意,参数已经在rdi,rsi和rdx中传递。我们 560 | ; 只需返回rax中的值即可。 561 | ; -------------------------------------------------- --------------------------- 562 | 563 | global maxofthree 564 | section .text 565 | maxofthree: 566 | mov rax,rdi ; 结果(rax)最初持有x 567 | cmp rax,rsi ; x小于y吗? 568 | cmovl rax,rsi ; 如果是这样,将结果设置为y 569 | cmp rax,rdx ; max(x,y)小于z吗? 570 | cmovl rax,rdx ; 如果是这样,将结果设置为z 571 | ret ; 最大值将为rax 572 | ``` 573 | 574 | 这是一个调用汇编语言函数的 C 程序。 575 | 576 | `callmaxofthree.c` 577 | 578 | ```c 579 | /* 580 | * 一个小程序,说明如何调用我们用汇编语言编写的maxofthree函数。 581 | * 582 | */ 583 | 584 | #include 585 | #include 586 | 587 | int64_t maxofthree (int64_t,int64_t,int64_t); 588 | 589 | int main(){ 590 | printf("%ld\n", maxofthree(1, -4, -7)); 591 | printf("%ld\n", maxofthree(2, -6, 1)); 592 | printf("%ld\n", maxofthree(2, 3, 1)); 593 | printf("%ld\n", maxofthree(-2, 4, 3)); 594 | printf("%ld\n", maxofthree(2, -6, 5)); 595 | printf("%ld\n", maxofthree(2, 4, 6)); 596 | return 0; 597 | } 598 | 599 | ``` 600 | 601 | ```shell 602 | $ nasm -felf64 maxofthree.asm && gcc callmaxofthree.c maxofthree.o && ./a.out 603 | 1 604 | 2 605 | 3 606 | 4 607 | 5 608 | 6 609 | ``` 610 | 611 | 612 | 613 | ## 有条件的指令 614 | 615 | 在算术或逻辑指令或比较指令之后`cmp`,处理器会设置或清除其中的位`rflags`。最有趣的标志是: 616 | 617 | - `s` (标志) 618 | - `z` (零) 619 | - `c` (携带) 620 | - `o` (溢出) 621 | 622 | 因此,执行完一条加法指令后,我们可以根据新的标志设置执行跳转,移动或设置。例如: 623 | 624 | | `jz` _标签_ | 如果运算结果为零,则跳至标签 L | 625 | | ---------------- | ------------------------------------------------------------ | 626 | | `cmovno` _x_,_y_ | _x ← y_ 如果最后的操作确实*不*溢出 | 627 | | `setc` _x_ | 如果最后一个操作带有进位,则*x* ← _1_,否则,_x_ ← *0,*否则(*x*必须是字节大小的寄存器或存储器位置) | 628 | 629 | 条件指令具有三种基本形式:`j`用于条件跳转,`cmov`用于条件移动和`set`用于条件设置。指令的后缀具有 30 种形式之一: `s ns z nz c nc o no p np pe po e ne l nl le nle g ng ge nge a na ae nae b nb be nbe`。 630 | 631 | ## 命令行参数 632 | 633 | 在 C 中,`main` 是一个古老而简单的函数,其实它自身可以附带一些参数: 634 | 635 | ```c 636 | int main(int argc, char ** argv) 637 | ``` 638 | 639 | 因此,您猜到了,`argc`以`rdi`结尾,而 `argv`(指针)以`rsi`结尾。下面运用这一点,实现将命令行参数简单地逐行显示的程序: 640 | 641 | `echo.asm` 642 | 643 | ```asm 644 | ; -------------------------------------------------- --------------------------- 645 | ; 一个显示其命令行参数(每行一个)的64位程序。 646 | ; 647 | ; 在输入时,rdi将包含argc,而rsi将包含argv。 648 | ; -------------------------------------------------- --------------------------- 649 | 650 | global main 651 | extern puts 652 | section .text 653 | 654 | main: 655 | push rdi ; 保存 puts 函数需要用到的寄存器 656 | push rsi 657 | sub rsp,8 ; 调用函数前让栈顶对齐 658 | 659 | mov rdi,[rsi] ; 需要输出的字符串参数 660 | call puts ; 调用 puts 输出 661 | 662 | add rsp,8 ; 恢复%rsp到未对齐的值 663 | pop rsi ; 恢复puts用到的寄存器 664 | pop rdi 665 | 666 | add rsi,8 ; 指向下一个参数 667 | dec rdi ; 递减参数计数 668 | jnz main ; 如果未读完参数则继续 669 | 670 | ret 671 | 672 | ``` 673 | 674 | ```shell 675 | $ nasm -felf64 echo.asm && gcc echo.o && ./a.out dog 22 -zzz "hi there" 676 | ./a.out 677 | dog 678 | 22 679 | -zzz 680 | hi there 681 | ``` 682 | 683 | 684 | 685 | ## 一个更长一些的例子 686 | 687 | 请注意,就 C library而言, 命令行参数总是以字符串的形式传入的。如果要将参数视为整数使用,请调用`atoi`函数。下面是一个计算 $x^y$ 的函数。 688 | 689 | `power.asm` 690 | 691 | ```asm 692 | ; -------------------------------------------------- --------------------------- 693 | ; 一个用于计算x ^ y的64位命令行应用程序。 694 | ; 695 | ; 语法:power x y 696 | ; x和y均是(32位)整数 697 | ; -------------------------------------------------- --------------------------- 698 | 699 | global main 700 | extern printf 701 | extern puts 702 | extern atoi 703 | 704 | section .text 705 | main: 706 | push r12 ; 调用者保存寄存器 707 | push r13 708 | push r14 709 | ; 通过压入三个寄存器的值, 栈已经对齐 710 | 711 | cmp rdi, 3 ; 必须有且仅有 2 个参数 712 | jne error1 713 | 714 | mov r12, rsi ; argv 715 | 716 | ; 我们将使用 ecx 作为指数的计数器, 直至 ecx 减到 0。 717 | ; 使用 esi 来保存基数, 使用 eax 保存乘积。 718 | 719 | mov rdi, [r12+16] ; argv[2] 720 | call atoi ; y in eax 721 | cmp eax, 0 ; 不允许负指数 722 | jl error2 723 | mov r13d, eax ; y in r13d 724 | 725 | mov rdi, [r12+8] ; argv 726 | call atoi ; x in eax 727 | mov r14d, eax ; x in r14d 728 | 729 | mov eax, 1 ; 初始结果 start with answer = 1 730 | check: 731 | test r13d, r13d ; 递减 y 直至 0 732 | jz gotit ; done 733 | imul eax, r14d ; 再乘上一个 x 734 | dec r13d 735 | jmp check 736 | gotit: ; print report on success 737 | mov rdi, answer 738 | movsxd rsi, eax 739 | xor rax, rax 740 | call printf 741 | jmp done 742 | error1: ; print error message 743 | mov edi, badArgumentCount 744 | call puts 745 | jmp done 746 | error2: ; print error message 747 | mov edi, negativeExponent 748 | call puts 749 | done: ; restore saved registers 750 | pop r14 751 | pop r13 752 | pop r12 753 | ret 754 | 755 | answer: 756 | db "%d", 10, 0 757 | badArgumentCount: 758 | db "Requires exactly two arguments", 10, 0 759 | negativeExponent: 760 | db "The exponent may not be negative", 10, 0 761 | 762 | ``` 763 | 764 | ```shell 765 | $ nasm -felf64 power.asm && gcc -o power power.o 766 | $ ./power 2 19 767 | 524288 768 | $ ./power 3 -8 769 | The exponent may not be negative 770 | $ ./power 1 500 771 | 1 772 | $ ./power 1 773 | Requires exactly two arguments 774 | ``` 775 | 776 | 777 | 778 | ## 浮点数指令 779 | 780 | 浮点数参数保存在 xmm 寄存器中。下面是一个用来计算存放在数组中的浮点数的和的简单的函数: 781 | 782 | `sum.asm` 783 | 784 | ```asm 785 | ; -------------------------------------------------- --------------------------- 786 | ; 一个64位程序,该函数返回浮点数数组元素之和 787 | ; 函数声明如下: 788 | ; 789 | ; double sum(double []array,uint64_t length) 790 | ; -------------------------------------------------- --------------------------- 791 | 792 | global sum 793 | section .text 794 | sum: 795 | xorpd xmm0, xmm0 ; 初始化累加和为 0 796 | cmp rsi, 0 ; 考虑数组长度为 0 的特殊情形 797 | je done 798 | next: 799 | addsd xmm0, [rdi] ; 累加当前数组元素的值 800 | add rdi, 8 ; 指向下一个数组元素 801 | dec rsi ; 计数器递减 802 | jnz next ; 如果计数器未归0,则继续累加 803 | done: 804 | ret ; 返回保存在 xmm0 寄存器中的值 805 | ``` 806 | 807 | 请注意,浮点指令具有`sd`后缀;这是最常见的一种,但稍后我们会再看到其他一些。这是一个调用它的 C 程序: 808 | 809 | `callum.c` 810 | 811 | ```c 812 | /** 813 | * 说明如何调用用汇编语言编写的sum函数。 814 | */ 815 | 816 | #include 817 | #include 818 | 819 | double sum(double[], uint64_t); 820 | 821 | int main() { 822 | double test[] = { 823 | 40.5, 26.7, 21.9, 1.5, -40.5, -23.4 824 | }; 825 | printf("%20.7f\n", sum(test, 6)); 826 | printf("%20.7f\n", sum(test, 2)); 827 | printf("%20.7f\n", sum(test, 0)); 828 | printf("%20.7f\n", sum(test, 3)); 829 | return 0; 830 | } 831 | ``` 832 | 833 | ```shell 834 | $ nasm -felf64 sum.asm && gcc sum.o callsum.c && ./a.out 835 | 26.7000000 836 | 67.2000000 837 | 0.0000000 838 | 89.1000000 839 | ``` 840 | 841 | 842 | 843 | ## 数据段 844 | 845 | 在大多数操作系统上,.data 数据段是只读的,所以你需要使用数据段。.data 部分仅仅用来初始化数据,而您还可以发现有一个特殊的.bss 的段,是用来存放未初始化过的数据的下面是一个程序用来计算通过命令行参数传递的整数的平均值,并且以浮点数输出结果的程序。 846 | 847 | `average.asm` 848 | 849 | ```asm 850 | ; -------------------------------------------------- --------------------------- 851 | ; 一个把参数当做整数处理, 并且以浮点数形式输出他们平均值的 64 位程序。 852 | ; 这个程序将使用一个数据段来保存中间结果。 853 | ; 这不是必须的, 但是在此我们想展示数据段是如何使用的。 854 | ; -------------------------------------------------- --------------------------- 855 | 856 | global main 857 | extern atoi 858 | extern printf 859 | default rel 860 | 861 | section .text 862 | main: 863 | dec rdi ; argc-1,因为我们不计算程序名称 864 | jz nothingToAverage 865 | mov [count], rdi ; 保存浮点数参数的个数 866 | accumulate: 867 | push rdi ; 保存调用 atoi 需要使用的寄存器 868 | push rsi 869 | mov rdi, [rsi+rdi*8] ; argv[rdi] 870 | call atoi ; 现在 rax 里保存着 arg 的整数值 871 | pop rsi ; 调用完 atoi 函数后恢复寄存器 872 | pop rdi 873 | add [sum], rax ; 继续累加 874 | dec rdi ; 递减 875 | jnz accumulate ; 还有参数吗? 876 | average: 877 | cvtsi2sd xmm0, [sum] 878 | cvtsi2sd xmm1, [count] 879 | divsd xmm0, xmm1 ; xmm0 现在值为 sum/count 880 | mov rdi, format ; printf 的第一个参数 [注: 输出格式] 881 | mov rax, 1 ; printf 是多参数的, 含有一个不是整数的参数 882 | 883 | sub rsp, 8 ; 对齐栈指针 884 | call printf ; printf(format, sum/count) 885 | add rsp, 8 ; 恢复栈指针 886 | 887 | ret 888 | 889 | nothingToAverage: 890 | mov rdi, error 891 | xor rax, rax 892 | call printf 893 | ret 894 | 895 | section .data 896 | count: dq 0 897 | sum: dq 0 898 | format: db "%g", 10, 0 899 | error: db "There are no command line arguments to average", 10, 0 900 | 901 | ``` 902 | 903 | ```shell 904 | $ nasm -felf64 average.asm && gcc average.o && ./a.out 19 8 21 -33 905 | 3.75 906 | $ nasm -felf64 average.asm && gcc average.o && ./a.out 907 | There are no command line arguments to average 908 | ``` 909 | 910 | 911 | 912 | 该程序着重介绍了一些在整数和浮点值之间转换的处理器指令。一些最常见的是: 913 | 914 | | `cvtsi2sd` _xmmreg,r/m32_ | _xmmreg [63..0]_ ← _intToDouble(r / m32)_ | 915 | | ----------------------------- | ----------------------------------------- | 916 | | `cvtsi2ss` _xmmreg,r/m32_ | _xmmreg [31..0]_ ← _intToFloat(r / m32)_ | 917 | | `cvtsd2si` _reg32_,_xmmr/m64_ | _reg32_ ← _doubleToInt(xmmr / m64)_ | 918 | | `cvtss2si` _reg32_,_xmmr/m32_ | _reg32_ ← _floatToInt(xmmr / m32)_ | 919 | 920 | ## 递归 921 | 922 | 也许令人惊讶的是,实现递归功能并没有什么不同寻常的。您只需要像往常一样小心保存寄存器的状态即可。在递归调用周围推动和弹出是一种典型的策略。 923 | 924 | `factorial.asm` 925 | 926 | ```asm 927 | ; ---------------------------------------------------------------------------- 928 | ; 一种递归函数的实现: 929 | ; 930 | ; uint64_t factorial(uint64_t n) { 931 | ; return (n <= 1) ? 1 : n * factorial(n-1); 932 | ; } 933 | ; ---------------------------------------------------------------------------- 934 | 935 | global factorial 936 | 937 | section .text 938 | factorial: 939 | cmp rdi, 1 ; n <= 1? 940 | jnbe L1 ; 如果不是, 进行递归调用 941 | mov rax, 1 ; 否则, 返回 1 942 | ret 943 | L1: 944 | push rdi ; 将n保存在堆栈上 (同时对齐 %rsp!) 945 | dec rdi ; n-1 946 | call factorial ; factorial(n-1), 返回值保保存到 %rax 947 | pop rdi ; 恢复 n 948 | imul rax, rdi ; n * factorial(n-1), 保存到 %rax 949 | ret 950 | ``` 951 | 952 | 一个调用示例: 953 | 954 | `callfactorial.c` 955 | 956 | ```c 957 | /** 958 | *这是一个调用外部定义的阶乘函数的程序。 959 | */ 960 | 961 | #include 962 | #include 963 | 964 | uint64_t factorial(uint64_t n); 965 | 966 | int main() { 967 | for (uint64_t i = 0; i < 20; i++) { 968 | printf("factorial(%2lu) = %lu\n", i, factorial(i)); 969 | } 970 | return 0; 971 | } 972 | ``` 973 | 974 | ```shell 975 | $ nasm -felf64 factorial.asm && gcc -std=c99 factorial.o callfactorial.c && ./a.out 976 | factorial( 0) = 1 977 | factorial( 1) = 1 978 | factorial( 2) = 2 979 | factorial( 3) = 6 980 | factorial( 4) = 24 981 | factorial( 5) = 120 982 | factorial( 6) = 720 983 | factorial( 7) = 5040 984 | factorial( 8) = 40320 985 | factorial( 9) = 362880 986 | factorial(10) = 3628800 987 | factorial(11) = 39916800 988 | factorial(12) = 479001600 989 | factorial(13) = 6227020800 990 | factorial(14) = 87178291200 991 | factorial(15) = 1307674368000 992 | factorial(16) = 20922789888000 993 | factorial(17) = 355687428096000 994 | factorial(18) = 6402373705728000 995 | factorial(19) = 121645100408832000 996 | ``` 997 | 998 | 999 | 1000 | ## SIMD 并行 1001 | 1002 | XMM 寄存器可以对浮点值进行一次算术运算(一次标量)或一次执行多次运算(打包)。操作具有以下形式: 1003 | 1004 | ```asm 1005 | op xmmreg_or_memory,xmmreg 1006 | ``` 1007 | 1008 | 对于浮点加法,说明如下: 1009 | 1010 | | `addpd` | 并行执行 2 个双精度加法(添加压缩双精度) | 1011 | | ------- | ------------------------------------------------------ | 1012 | | `addsd` | 使用寄存器的低 64 位仅执行一次双精度加法(将标量加倍) | 1013 | | `addps` | 并行执行 4 个单精度加法(添加打包的单个) | 1014 | | `addss` | 使用寄存器的低 32 位仅执行一个单精度加法(加标量单) | 1015 | 1016 | 这是一个可以一次添加四个浮点数的函数: 1017 | 1018 | `add_four_floats.asm` 1019 | 1020 | ```asm 1021 | ; void add_four_floats(float x[4], float y[4]) 1022 | ; x[i] += y[i] for i in range(0..4) 1023 | 1024 | global add_four_floats 1025 | section .text 1026 | 1027 | add_four_floats: 1028 | movdqa xmm0, [rdi] ; x的所有四个值 1029 | movdqa xmm1, [rsi] ; y的所有四个值 1030 | addps xmm0, xmm1 ; 一次完成所有四个总和 1031 | movdqa [rdi], xmm0 1032 | ret 1033 | ``` 1034 | 1035 | 一个呼叫者: 1036 | 1037 | `test_add_four_floats.c` 1038 | 1039 | ```c 1040 | #include 1041 | void add_four_floats(float[], float[]); 1042 | 1043 | int main() { 1044 | float x[] = {-29.750, 244.333, 887.29, 48.1E22}; 1045 | float y[] = {29.750, 199.333, -8.29, 22.1E23}; 1046 | add_four_floats(x, y); 1047 | printf("%f\n%f\n%f\n%f\n", x[0], x[1], x[2], x[3]); 1048 | return 0; 1049 | } 1050 | ``` 1051 | 1052 | 还可以参考:[nice little x86 floating-point slide deck from Ray Seyfarth](http://rayseyfarth.com/asm/pdf/ch11-floating-point.pdf) 1053 | 1054 | ## 饱和运算 1055 | 1056 | XMM 寄存器还可以对整数进行算术运算。这些说明具有以下形式: 1057 | 1058 | ```asm 1059 | op xmmreg_or_memory, xmmreg 1060 | ``` 1061 | 1062 | 对于整数加法,说明如下: 1063 | 1064 | | `paddb` | 做 16 个字节加法 | 1065 | | --------- | ------------------------------------------ | 1066 | | `paddw` | 做 8 个单词加法 | 1067 | | `paddd` | 做 4 个 dword 加法 | 1068 | | `paddq` | 做 2 个 qword 加法 | 1069 | | `paddsb` | 执行 16 个字节加法并带符号饱和度(80..7F) | 1070 | | `paddsw` | 进行 8 个带符号饱和的单词加法(8000..7F) | 1071 | | `paddusb` | 执行 16 个字节的无符号饱和(00..FF) | 1072 | | `paddusw` | 进行 8 个无符号饱和(00..FFFF)的单词加法 | 1073 | 1074 | 这是一个例子。它还说明了如何加载 XMM 寄存器。您无法加载立即值;您必须使用它`movaps`来移出内存。还有其他方法,但是我们不会在本教程中介绍所有内容。 1075 | 1076 | `satexample.asm` 1077 | 1078 | ```asm 1079 | ; ---------------------------------------------------------------------------------------- 1080 | ; 有符号饱和运算示例。 1081 | ; ---------------------------------------------------------------------------------------- 1082 | global main 1083 | extern printf 1084 | 1085 | section .text 1086 | main: 1087 | push rbp 1088 | movaps xmm0, [arg1] 1089 | movaps xmm1, [arg2] 1090 | paddsw xmm0, xmm1 1091 | movaps [result], xmm0 1092 | 1093 | lea rdi, [format] 1094 | mov esi, dword [result] 1095 | mov edx, dword [result+4] 1096 | mov ecx, dword [result+8] 1097 | mov r8d, dword [result+12] 1098 | xor rax, rax 1099 | call printf 1100 | pop rbp 1101 | ret 1102 | section .data 1103 | align 16 1104 | arg1: dw 0x3544,0x24FF,0x7654,0x9A77,0xF677,0x9000,0xFFFF,0x0000 1105 | arg2: dw 0x7000,0x1000,0xC000,0x1000,0xB000,0xA000,0x1000,0x0000 1106 | result: dd 0, 0, 0, 0 1107 | format: db '%x%x%x%x',10,0 1108 | ``` 1109 | 1110 | 1111 | 1112 | ## 图形 1113 | 1114 | TODO 1115 | 1116 | ## 局部变量和堆栈框架 1117 | 1118 | 首先,请阅读[Eli Bendersky 的文章,](http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/) 该概述比我的简短笔记更完整。 1119 | 1120 | 调用函数时,调用者将首先将参数存入正确的寄存器中,然后发出`call`指令。调用之前,超出寄存器覆盖范围的其他参数将被推入栈中。所调用的指令会把返回地址存入栈顶。所以如果有以下的函数: 1121 | 1122 | ``` 1123 | int64_t example(int64_t x, int64_t y) { 1124 | int64_t a, b, c; 1125 | b = 7; 1126 | return x * b + y; 1127 | } 1128 | ``` 1129 | 1130 | 在函数的入口,x 将存在 edi 中,y 将存在 esi 中,返回地址将在堆栈的顶部。局部变量会被存到哪里?无论是否有足够的寄存器,一种简单的选择就是存入函数自己的栈中。 1131 | 1132 | 如果程序运行在一个实现了 ABI 标准的机器上,你可以在 rsp 保持不变的情况下获取无法在寄存器中保存的参数值和局部变量值,例如: 1133 | 1134 | ```shell 1135 | +----------+ 1136 | rsp-24 | a | 1137 | +----------+ 1138 | rsp-16 | b | 1139 | +----------+ 1140 | rsp-8 | c | 1141 | +----------+ 1142 | rsp | retaddr | 1143 | +----------+ 1144 | rsp+8 | caller's | 1145 | | stack | 1146 | | frame | 1147 | | ... | 1148 | +----------+ 1149 | ``` 1150 | 1151 | 因此,我们的函数看上去是这个样子的: 1152 | 1153 | ```asm 1154 | global example 1155 | section .text 1156 | example: 1157 | mov qword [rsp-16], 7 1158 | mov rax, rdi 1159 | imul rax, [rsp+8] 1160 | add rax, rsi 1161 | ret 1162 | ``` 1163 | 1164 | 如果被调用的函数需要调用其他函数,你就需要调整 rsp 的值来得到正确的返回地址。 1165 | 1166 | 在 Windows 上,您不能使用此方案,因为当中断发生时,栈指针上方的所有内容都会被抹去。而在其他大多数操作系统中,不会发生这种情况,因为在栈指针后面,有一个 128 字节的"红色区域”来保护栈指针的安全。在这种情况下,您可以立即在栈上腾出空间: 1167 | 1168 | ```gas 1169 | 例: 1170 | sub rsp, 24 1171 | ``` 1172 | 1173 | 因此我们的堆栈如下所示: 1174 | 1175 | ``` 1176 | +----------+ 1177 | rsp | a | 1178 | +----------+ 1179 | rsp+8 | b | 1180 | +----------+ 1181 | rsp+16 | c | 1182 | +----------+ 1183 | rsp+24 | retaddr | 1184 | +----------+ 1185 | rsp+32 | caller's | 1186 | | stack | 1187 | | frame | 1188 | | ... | 1189 | +----------+ 1190 | 1191 | ``` 1192 | 1193 | 现在是这里的功能。请注意,我们必须记住在返回之前要替换栈指针! 1194 | 1195 | ```asm 1196 | global example 1197 | section .text 1198 | example: 1199 | sub rsp, 24 1200 | mov qword [rsp+8], 7 1201 | mov rax, rdi 1202 | imul rax, [rsp+8] 1203 | add rax, rsi 1204 | add rsp, 24 1205 | ret 1206 | ``` 1207 | 1208 | ## 在 macOS 上使用 NASM 1209 | 1210 | 希望您已经使用基于 Linux 的操作系统(或更正确地说,是 ELF64 系统)阅读了以上整个教程。要使这些示例在 64 位 macOS 系统上工作,几乎只有五件事要了解: 1211 | 1212 | - 此目标文件格式`macho64`不是`elf64`。 1213 | - 系统调用值***完全不同***。 1214 | - 模块之间**共享符号**将**以下划线作为前缀**。 1215 | - 除非您进行一些设置调整,否则 macOS 中的 gcc 链接器似乎不允许绝对寻址。因此`default rel`,在引用标记的内存位置时添加,并且始终用`lea`获取地址。 1216 | - 另外,似乎有时在 Linux 下,不强制执行 16 位堆栈对齐要求,但似乎*始终*在 macOS 下强制执行。 1217 | 1218 | 因此,这是上面为 macOS 编写的普通程序。 1219 | 1220 | `average.asm` 1221 | 1222 | ```asm 1223 | ; -------------------------------------------------- --------------------------- 1224 | ; 64位程序,将其所有命令行参数都视为整数和 1225 | ; 将其平均值显示为浮点数。该程序使用数据 1226 | ; 存储中间结果的部分,不是必须要存储的,而仅仅是 1227 | ; 说明如何使用数据段。 1228 | ; 1229 | ; 为OS X设计。组装和运行: 1230 | ; 1231 | ; nasm -fmacho64 average.asm && gcc average.o && ./a.out 1232 | ; -------------------------------------------------- --------------------------- 1233 | 1234 | global _main 1235 | extern _atoi 1236 | extern _printf 1237 | default rel 1238 | 1239 | section .text 1240 | _main: 1241 | push rbx ; 我们从未使用,但是必须的代码,对齐堆栈,以便我们调用东西 1242 | dec rdi ; argc-1, 因为我们不计算程序名称 1243 | jz nothingToAverage 1244 | mov [count], rdi ; 保存实参数量 1245 | accumulate: 1246 | push rdi ; save register across call to atoi 1247 | push rsi 1248 | mov rdi, [rsi+rdi*8] ; argv[rdi] 1249 | call _atoi ; now rax has the int value of arg 1250 | pop rsi ; restore registers after atoi call 1251 | pop rdi 1252 | add [sum], rax ; accumulate sum as we go 1253 | dec rdi ; count down 1254 | jnz accumulate ; more arguments? 1255 | average: 1256 | cvtsi2sd xmm0, [sum] 1257 | cvtsi2sd xmm1, [count] 1258 | divsd xmm0, xmm1 ; xmm0 is sum/count 1259 | lea rdi, [format] ; 1st arg to printf 1260 | mov rax, 1 ; printf is varargs, there is 1 non-int argument 1261 | call _printf ; printf(format, sum/count) 1262 | jmp done 1263 | 1264 | nothingToAverage: 1265 | lea rdi, [error] 1266 | xor rax, rax 1267 | call _printf 1268 | 1269 | done: 1270 | pop rbx ; undoes the stupid push at the beginning 1271 | ret 1272 | 1273 | section .data 1274 | count: dq 0 1275 | sum: dq 0 1276 | format: db "%g", 10, 0 1277 | error: db "There are no command line arguments to average", 10, 0 1278 | ``` 1279 | 1280 | ```shell 1281 | $ nasm -fmacho64 average.asm && gcc average.o && ./a.out 1282 | There are no command line arguments to average 1283 | $ nasm -fmacho64 average.asm && gcc average.o && ./a.out 54.3 1284 | 54 1285 | $ nasm -fmacho64 average.asm && gcc average.o && ./a.out 54.3 -4 -3 -25 455.1111 1286 | 95.4 1287 | ``` 1288 | 1289 | 1290 | 1291 | ## 在 Windows 上使用 NASM 1292 | 1293 | 我不确定 Windows 上的 syscall 是什么,但是我确实知道,如果您要汇编和链接 C 库,则必须了解[x64 约定](https://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx)。阅读它们。您将学到以下内容: 1294 | 1295 | - 前四个整数参数在 RCX,RDX,R8 和 R9 中传递。其余的将被推入堆栈。 1296 | - 被调用者(callee)必须保留 RBX,RBP,RDI,RSI,RSP,R12,R13,R14 和 R15。 1297 | - 您猜对了,前四个浮点参数被通过 XMM0,XMM1,XMM2 和 XMM3。 1298 | - 返回值进入 RAX 或 XMM0。 1299 | 1300 | **重要说明**:在任何文档中都很难找到一件事:x64 调用约定要求您在每次调用之前分配 32 字节的[影子空间](http://stackoverflow.com/a/30191127/831878),并在调用之后将其删除。这意味着您的"hello world"程序如下所示: 1301 | 1302 | `hello.asm` 1303 | 1304 | ```asm 1305 | ; -------------------------------------------------- -------------------------------------- 1306 | ; 这是一个Win64控制台程序,它在一行上写入" Hello",然后退出。它 1307 | ; 使用C库中的puts。编译汇编代码并运行: 1308 | ; 1309 | ; nasm -fwin64 hello.asm && gcc hello.obj && a 1310 | ; -------------------------------------------------- -------------------------------------- 1311 | 1312 | global main 1313 | extern puts 1314 | section .text 1315 | main: 1316 | sub rsp, 28h ; 保留影子空间 1317 | mov rcx, message ; 第一个参数是message变量的地址 1318 | call puts ; puts(message) 1319 | add rsp, 28h ; 删除影子空间 1320 | ret 1321 | message: 1322 | db 'Hello', 0 ; C strings need a zero byte at the end 1323 | 1324 | ``` 1325 | 1326 | 您是否注意到我们实际上保留了 40 个字节?最小 32 字节的影子空间是必需的。在我们的`main`函数中,我们正在调用另一个函数,因此我们的堆栈[必须在 16 字节边界处对齐](https://docs.microsoft.com/en-us/cpp/build/stack-usage)。当`main`被调用时,返回地址(8 个字节)被压入,因此我们必须向影子空间"添加"额外的 8 个字节。 1327 | 1328 | 1329 | --------------------------------------------------------------------------------