为什么不呢?玩软件很有趣,对吧?
我知道这听起来很疯狂;为了好玩而聚在一起——但相信我,你会感觉活过来了。
我很幸运能够构建几乎所有类型的东西,所以我正在尝试开发一个底层系列,结合了Node.js和汇编作为共享库函数。
在本文中,我们将用x86 Linux汇编语言跟世界打招呼。
……
在汇编语言中的问候语你需要一个Linux环境。在Windows上,你可以用WSL2,这很容易做到。
创建一个名为 hello.s
的文件,并在其中添加以下代码:
.section .data
msg:
.asciz "Hello, World!\n" # 定义一个以空字符结尾并带有换行符的字符串字面量
msg_len = . - msg # 从当前位置到msg的偏移量
.section .text
.globl _start # 将_start定义为全局符号(入口点)
_start:
# --- sys_write(int fd, const void *buf, size_t count) ---
movl $4, %eax # 将sys_write的系统调用号放入%eax
movl $1, %ebx # 将stdout文件描述符放入%ebx
movl $msg, %ecx # msg地址
movl $msg_len, %edx # msg长度
int $0x80 # 执行内核系统调用
# --- sys_exit(int status) ---
movl $1, %eax # 将sys_exit的系统调用号放入%eax
xorl %ebx, %ebx # 清除%ebx寄存器(退出码为0)
int $0x80 # 发出内核系统调用
进入全屏 退出全屏
使用as
来编译程序。在Linux上安装很简单,只需通过这个命令安装这些GNU工具:
执行以下命令以更新软件包列表并安装必要的开发工具:
sudo apt update && sudo apt install -y build-essential gdb libgtk-3-dev
点击全屏切换按钮
然后编译你的汇编代码文件。
汇编 ./hello.s -o hello.o # 汇编 hello.s 文件并输出为 hello.o 文件 (Assembles the hello.s file and outputs it as hello.o)
点击这里进入全屏模式:全屏模式 点击这里退出:退出全屏
这条命令生成了目标文件。接下来,链接它一下。
ld ./hello.o -o hello # 这里我们使用ld命令将hello.o链接到可执行文件hello
全屏 退出全屏
最后一步,运行一下你的程序:
./hello
运行这个命令来打招呼。
全屏模式 全屏退出
这可能是你见过的最复杂的"Hello World"程序之一,特别是如果你是汇编语言的新手。
我们把它拆开来看看怎么样?
……
装配中的部分汇编程序通常分为几个部分:
.section .data
→ 我们在这里存储数据,比如 'Hello, World!'.section .text
→ 我们在这里编写指令.globl _start
→ 定义程序的起点
符号与标签
符号是用来表示内存中的位置的名字。
例如,msg
是一个符号。它告诉编译器或汇编器,“记住这个位置,我们之后会用到它。” 它就像一个变量一样,这样更符合中文的表达习惯。
当你加上一个 :
时,它就变成一个标签了,定义了这个符号的值。
msg:
.asciz "喂,地球!\n"
切换到全屏模式,退出全屏
这表示:在内存位置 msg,存放字符串 "Hello, World!\n "。(在引号后加上一个空格以增强可读性)
这种模式无处不在。例如,_start
也是一个标签:
.globl _start // 全局符号_start,程序的入口点
_start:
全屏模式 退出全屏
把 _start
设为全局,这样汇编器和链接器就能找到它了。
此处省略内容
内存监控程序或代码不会自动管理内存——你需要手动完成。
消息长度 = 消息长度 - 消息
这里出了什么事?
msg
只是一个指向。它指向了 "Hello, World!\n"
的开头部分,但并不知道它在哪里结束。\n
.
表示当前内存位置,在字符串之后。
所以,msg_len = . - msg
意味着:
取
"Hello, World!\n"
之后的地址值,然后减去起点地址。
这样我们就得到了字符串的长度,这是我们调用 sys_write
需要的。
CPU 架构
这是一张CPU的图片。
寄存器暂时存放数据,运行速度远超RAM。
在我们的程序里,%eax
是一个寄存器。
通用用途寄存器:
%eax
— 这是EAX寄存器,用于存储和操作数据。%ebx
— 这是EBX寄存器,常用于存储地址或数据。%ecx
— 这是ECX寄存器,通常用于计数操作。%edx
— 这是EDX寄存器,常与EAX一起使用进行数据处理。%edi
— 这是EDI寄存器,通常用于指针操作。%esi
— 这是ESI寄存器,通常用于指针操作。
专用寄存器:
%ebp
%esp
%eip
%eflags
这是我们如何在寄存器之间传输数据的方法。
movl $4, %eax # 将 4 放入 %eax 并设置为 sys_write
movl $1, %ebx # 将文件描述符 1 放入 %ebx
movl $msg, %ecx # 消息指针
movl $msg_len, %edx # 长度
int $0x80 # 调用内核中断 0x80
全屏模式 退出全屏
但在我们做任何事之前,我们得先谈谈……
核心部分
内核是你的程序和硬件资源之间的中介。例如,所有的系统调用(例如访问文件、分配内存、退出程序之类)都要通过内核。
每次你看见这个:
int 0x80 /* 系统调用 */
切换到全屏 退出全屏
你正在向内核寻求帮助,对吧?
但是内核有自己的规矩。在调用它之前,你必须用正确的值来配置寄存器。
例如,要关闭一个程序,内核需要:
movl $1, %eax # 退出系统调用
movl $0, %ebx # 退出码
全屏模式 退出全屏
The sys_exit
需要:
%eax = 1
→ "我想退出。"%ebx = 0
→ "成功退出。"
这就像在C语言中返回0一样
int main() {
return 0; // Exit status 0 (success)
}
进入全屏 退出全屏
控制台输出
与 sys_exit
不同,向终端写入需要更多的信息:
movl $4, %eax # sys_write系统调用
movl $1, %ebx # 标准输出文件描述符1
movl $msg, %ecx # 消息的地址
movl $msg_len, %edx # 消息长度:
int $0x80 # 内核中断调用
点击全屏,再次点击退出全屏。
事情是这样的:
movl $4, %eax
→ 数字 4 表示,我想写入数据。movl $1, %ebx
→ 数字 1 表示写入到标准输出。movl $msg, %ecx
→ 消息在哪里?消息就在msg
这个位置。movl $msg_len, %edx
→ 消息的长度是多少?它在msg_len
这个变量里。int $0x80
→ 调用内核来执行写入操作。
写好消息后,我们就退出了:
movl $1, %eax # 退出系统调用
xorl %ebx, %ebx # 退出码为0
int $0x80 # 中断调用
进入全屏 退出全屏
AT&T 与 Intel 的语法之争.
此程序使用AT&T语法格式,这种语法格式在GNU汇编器中很常见。
在 NASM 中使用的 Intel 语法看起来略有不同。一个主要的区别在于,AT&T 语法会在寄存器前加上 %
符号。
大多数书籍都使用这种 AT&T 语法,但有些人更喜欢这种 Intel 语法,所以学学还是有必要的。
太棒了!你做到了!
就这样就行了!你刚刚就写了个并理解了这个x86汇编的"Hello, World!"
我可能会开始一个初级的Node.js教程系列。如果这听起来不错,告诉我一声!
你可以在x上找到我。
同时,你可以看看这个系列,在这里我们将从头开始构建一个原生的 Node 消息中间件:
超越API和端点:构建一个使用TCP的消息代理 payhip.com,这是一张网站图标的图片
下面是你将学到的内容:
- 带有心跳功能的 TCP 永远在线
- 缓冲区
- 队列确认与清理
- 基于事件和事件队列的客户端驱动
- 发布/订阅
- 持久队列的 BSON 编码和解码
- 握手和认证 🚀
共同学习,写下你的评论
评论加载中...
作者其他优质文章