assembly for mac part 1 - ltoddy/blog GitHub Wiki
最近看到有人在讨论汇编,我也好久没写汇编了,过去都是在ubuntu上写汇编的,毕业之后换了macbook pro,心血来潮,想再我的mbp写点汇编复习一下,发现和在ubuntu上写汇编区别不是很大, 我是用的是x86 64架构。
$ uname -m
x86_64
首先你需要一个汇编器,我这里使用nasm的,开源免费,基本上是用的最广泛的。
$ brew install nasm
此次你需要一个写代码的软件,编辑器(vscode, atom, emacs),ide(clion)都可以,只要有个语法高亮就行,毕竟大家不会用汇编写很大的程序。
我的选择是clion + nasm assembly language(这是一个插件)
如果你学过编程,相比对hello world
并不陌生。
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Hello world!\n");
exit(0);
}
打印hello world
程序被称为第一个演示程序。他有着极其重要的地位。
来看看汇编是如何实现它的.
hello.asm
section .data
message db "Hello world!", 0x0a
section .text
global main
main:
mov rax, 0x2000004
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
mov rax, 0x2000001
mov rdi, 0
syscall
试着来运行一下:
$ nasm -f macho64 -o hello.o hello.asm
$ ld -static -o hello -e main hello.o
$ ./hello
Hello world!
回头来看看代码,如果你过去没学过汇编,那么肯定会看的一头雾水,没关系慢慢来。
首先,代码中有两个名叫section
的东西:
- .data: 这里用来声明初始化的数据或常量
- .text: 这里用来编写所要执行的代码
在data
段里面:
message db "Hello world!", 0x0a
我将"Hello world!"字符串放进了message
常量中.
在.text
段中:
global main
你还记得最开始学c语言的时候,老师怎么说的嘛,需要定义一个main
函数,它是程序的入口。
这里,我们就需要声明他。
main:
mov rax, 0x2000004
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
mov rax, 0x2000001
mov rdi, 0
syscall
在我们的main
中, 有上下两个部分,分别的作用就是本文中c程序main
函数的那两行代码。
- print:
mov rax, 0x2000004
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
首先需你要知道 mac 系统调用,
我们需要一个用于输出的系统调用write
.
可以看到:
4 AUE_NULL ALL { user_ssize_t write(int fd, user_addr_t cbuf, user_size_t nbyte); }
它是第四号系统调用,参数1为文件描述符,参数2是你要输出的buffer,参数3是buffer的长度。
在这里,我们想要打印hello world,所以参数1应该为stdout
,也就是1
.
参数2为我们想要打印的Hello world!\n
, 这一共13个字符长度。
- exit
mov rax, 0x2000001
mov rdi, 0
syscall
最后我们需要退出程序exit
1 AUE_EXIT ALL { void exit(int rval); }
它是一号系统调用,需要一个参数,也就是退出的时候的返回码。
一些寄存器的使用:
- rax: 临时寄存器,当我们调用syscall的时候,ras必须包含syscall的号
- rdi: 函数的第一个参数
- rsi: 函数的第二个参数
- rdx: 函数的第三个参数
- rcx: 函数的第四个参数
- r8: 函数的第五个参数
- r9: 函数的第六个参数
在代码里面,当我们想要调用1号系统调用的时候: mov rax, 0x2000001
, 为什么不是mov rax, 1
。
如果你是linux系统的话,mov rax, 1
这么写是对的,但是在mac上就得mov rax, 0x2000001
, 但是为什么呢?
这个原因,我查找了好几个小时。
/*
* Syscall classes for 64-bit system call entry.
* For 64-bit users, the 32-bit syscall number is partitioned
* with the high-order bits representing the class and low-order
* bits being the syscall number within that class.
* The high-order 32-bits of the 64-bit syscall number are unused.
* All system classes enter the kernel via the syscall instruction.
*
* These are not #ifdef'd for x86-64 because they might be used for
* 32-bit someday and so the 64-bit comm page in a 32-bit kernel
* can use them.
*/
#define SYSCALL_CLASS_SHIFT 24
#define SYSCALL_CLASS_MASK (0xFF << SYSCALL_CLASS_SHIFT)
#define SYSCALL_NUMBER_MASK (~SYSCALL_CLASS_MASK)
#define SYSCALL_CLASS_NONE 0 /* Invalid */
#define SYSCALL_CLASS_MACH 1 /* Mach */
#define SYSCALL_CLASS_UNIX 2 /* Unix/BSD */
#define SYSCALL_CLASS_MDEP 3 /* Machine-dependent */
#define SYSCALL_CLASS_DIAG 4 /* Diagnostics */
/* Macros to simpllfy constructing syscall numbers. */
#define SYSCALL_CONSTRUCT_MACH(syscall_number) \
((SYSCALL_CLASS_MACH << SYSCALL_CLASS_SHIFT) | \
(SYSCALL_NUMBER_MASK & (syscall_number)))
#define SYSCALL_CONSTRUCT_UNIX(syscall_number) \
((SYSCALL_CLASS_UNIX << SYSCALL_CLASS_SHIFT) | \
(SYSCALL_NUMBER_MASK & (syscall_number)))
#define SYSCALL_CONSTRUCT_MDEP(syscall_number) \
((SYSCALL_CLASS_MDEP << SYSCALL_CLASS_SHIFT) | \
(SYSCALL_NUMBER_MASK & (syscall_number)))
#define SYSCALL_CONSTRUCT_DIAG(syscall_number) \
((SYSCALL_CLASS_DIAG << SYSCALL_CLASS_SHIFT) | \
(SYSCALL_NUMBER_MASK & (syscall_number)))
在mac os上或者bsd上,也就是unix like这样的系统上,它对系统调用做了分类了。
当去使用32位系统调用的时候,我们的64位寄存器,高32位不使用,只使用低32位。
而在使用的低32位里面,最高的字节, #define SYSCALL_CLASS_UNIX 2
,为2.
也就是,在我们的mac上,一个完成的系统调用号位(这里用二进制表示:) 0b00000010_00000000_00000000_00000000
+ 系统调用号
所以才有了0x2000001
这个挺奇怪的数字。