首先使用Homebrew安装nasm

1
brew install 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
/* 8 old creat */
#define SYS_link 9
#define SYS_unlink 10
/* 11 old execv */
#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

2022-01-04
Contents

⬆︎TOP