assembly for mac part 1 - ltoddy/blog GitHub Wiki

在mac上写汇编 part 1

最近看到有人在讨论汇编,我也好久没写汇编了,过去都是在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

如果你学过编程,相比对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函数的那两行代码。

  1. 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个字符长度。

  1. 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这个挺奇怪的数字。

⚠️ **GitHub.com Fallback** ⚠️