首先使用Homebrew安装nasm
随后创建 hello.asm 文件,写入如下指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| SECTION .data
msg: db "Hello Assembly! ", 0x0a len: equ $-msg
SECTION .text global _main
_main: mov rax, 0x2000004 mov rdi, 1 mov rsi, msg mov rdx, len syscall
mov rax, 0x2000001 mov rdi, 0 syscall
|
这里使用到了 id 为 4 的 syscall 系统调用。
我们前往/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk 1/usr/include/sys/这个目录,找到一个叫syscall.h的文件。这个文件的格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #ifndef _SYS_SYSCALL_H_ #define _SYS_SYSCALL_H_
#include <sys/appleapiopts.h> #ifdef __APPLE_API_PRIVATE #define SYS_syscall 0 #define SYS_exit 1 #define SYS_fork 2 #define SYS_read 3 #define SYS_write 4 #define SYS_open 5 #define SYS_close 6 #define SYS_wait4 7 #define SYS_link 9 #define SYS_unlink 10 #define SYS_chdir 12 #define SYS_fchdir 13
|
第二列是系统调用的名字,第三列是系统调用号。 系统调用的名字很直白地表述了系统调用的作用,比如说SYS_exit就是退出进程,SYS_fork就是创建进程,SYS_read就是打开文件等等。 系统调用实质上是操作系统提供给我们的一个C函数接口。
可以看到,id 为 4 的系统调用名字是SYS_write。
前往Apple官方的开源网站opensource.apple,然后会发现每个版本的macOS都有一部分开源的文件。进入任意一个版本的开源目录下,可以找到一个以xnu开头的目录。这就是每个版本的内核代码,直接下载即可。如果不在意版本号,那么可以直接前往其在GitHub上的镜像apple/darwin-xnu下载即可。
在下载好的xnu目录下,前往子目录 bsd/kern/ 中,找到一个文件syscalls.master. 这就是所有系统调用的函数原型。我们可以利用命令行工具cat进行查看。其文件格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <sys/appleapiopts.h> #include <sys/param.h> #include <sys/systm.h> #include <sys/types.h> #include <sys/sysent.h> #include <sys/sysproto.h> #include <nfs/nfs_conf.h>
0 AUE_NULL ALL { int nosys(void); } { indirect syscall } 1 AUE_EXIT ALL { void exit(int rval) NO_SYSCALL_STUB; } 2 AUE_FORK ALL { int fork(void) NO_SYSCALL_STUB; } 3 AUE_NULL ALL { user_ssize_t read(int fd, user_addr_t cbuf, user_size_ t nbyte); } 4 AUE_NULL ALL { user_ssize_t write(int fd, user_addr_t cbuf, user_size _t nbyte); } 5 AUE_OPEN_RWTC ALL { int open(user_addr_t path, int flags, int mode) NO _SYSCALL_STU
|
其第一列是系统调用号,第四列则是函数原型。
使用系统调用和使用系统库函数类似,但是,系统库函数我们可以利用函数名进行调用,如_exit, _printf等。但是,我们使用系统调用,则只能利用系统调用号进行调用。这里还有一点需要注意的,就是之前在操作系统基础中提到过,macOS的内核XNU是分为BSD层和Mach层。我们常用的系统调用都属于BSD的系统调用。而BSD层在逻辑地址上是位于Mach层之上的,BSD层要从0x2000000开始。因此,我们实际使用的调用号应该是syscall.h给出的调用号加上0x2000000之后的结果,如SYS_exit的调用号就应当是0x2000001
所以再回过头看前面的汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| SECTION .data ; 在显示器上输出Hello Assembly! ; _main的前段系统调用了write,后半段系统调用了exit ; write(int fd, const void *buffer, size_t nbytes) ; exit(int status)
msg: db "Hello Assembly! ", 0x0a ;0x0a就是C语言的'\0'字符串结束符 len: equ $-msg ;变量len等于msg的长度 SECTION .text global _main _main: mov rax,0x2000004 ;0x2000004 表示 syscall 调用号 write mov rdi,1 ;第一个参数:文件描述符(fd),1代表标准输出stdout,也就是fd的值 mov rsi,msg ;第二个参数:要输出的字节序列(buffer),syscall 调用会到 rsi 来获取字符 mov rdx,len ;第三个参数:字节序列的长度 syscall mov rax,0x2000001 ;0x2000001 表示退出 syscall mov rdi,0 syscall ; 通过 syscall 指令进行系统调用时,约定的传递参数的寄存器依次为 rdi、rsi、rdx、rcx
|
编译:
1
| nasm -f macho64 -o 可重定位目标程序文件名.o -g 源代码文件名.asm
|
编译完成以后会生成 *.o 链接文件。链接:
1
| ld -o 可执行文件文件名 -e _main 可重定位目标程序文件名.o -macosx_version_min 10.15 -static
|
运行 hello
调试使用gdb。参考:https://blog.csdn.net/BlingblingFu/article/details/108932799
另外,main 函数结束后使用 exit 系统调用和 retq 的区别,请参考:https://blog.csdn.net/EvianZhang/article/details/96702864