用汇编语言写一个hello world,并进行汇编和编译

文章目录

  • 一、程序编写
  • 二、解释hello.asm的每一行代码
    • [1. 数据段定义 (`.section .data`)](#1. 数据段定义 (.section .data))
    • [2. 代码段与入口点 (`.section .text`)](#2. 代码段与入口点 (.section .text))
    • [3. 输出字符串 (sys_write)](#3. 输出字符串 (sys_write))
    • [4. 退出程序 (sys_exit)](#4. 退出程序 (sys_exit))
  • 三、介绍用到的寄存器
    • [1. `rax` (Accumulator Register / 累加器)](#1. rax (Accumulator Register / 累加器))
    • [2. `rdi` (Destination Index Register / 目的变址寄存器)](#2. rdi (Destination Index Register / 目的变址寄存器))
    • [3. `rsi` (Source Index Register / 源变址寄存器)](#3. rsi (Source Index Register / 源变址寄存器))
    • [4. `rdx` (Data Register / 数据寄存器)](#4. rdx (Data Register / 数据寄存器))
    • [💡 扩展知识:Linux x86-64 系统调用的完整传参顺序](#💡 扩展知识:Linux x86-64 系统调用的完整传参顺序)
    • [⚠️ 一个容易混淆的细节:寄存器的大小后缀](#⚠️ 一个容易混淆的细节:寄存器的大小后缀)

一、程序编写

这里我以最经典的 x86-64 Linux 汇编语言 (NASM) 为例,为你编写一个 "Hello, World!" 程序,并给出完整的编译和运行步骤。

1. 编写汇编代码

创建一个名为 hello.asm 的文件,写入以下代码:

nasm 复制代码
; hello.asm - x86-64 Linux Hello World (NASM)

section .data
    msg db "Hello, World!", 10  ; 字符串内容,10 是换行符 '\n'
    msg_len equ $ - msg         ; 计算字符串长度

section .text
    global _start               ; 链接器入口点

_start:
    ; 系统调用: sys_write (向标准输出写数据)
    mov rax, 1                  ; 系统调用号 1 = sys_write
    mov rdi, 1                  ; 文件描述符 1 = stdout (标准输出)
    mov rsi, msg                ; 指向要输出的字符串地址
    mov rdx, msg_len            ; 要输出的字节数
    syscall                     ; 触发系统调用

    ; 系统调用: sys_exit (退出程序)
    mov rax, 60                 ; 系统调用号 60 = sys_exit
    xor rdi, rdi                ; 退出码 0 (表示成功)
    syscall                     ; 触发系统调用

2. 汇编与编译过程

在 Linux 环境下(确保已安装 nasmld),依次执行以下命令:

第一步:汇编 (Assemble)

将汇编源代码转换为机器码目标文件:

bash 复制代码
nasm -f elf64 hello.asm -o hello.o

说明:-f elf64 指定生成 64位 ELF 格式的目标文件。

第二步:链接 (Link)

将目标文件链接为可执行文件:

bash 复制代码
ld hello.o -o hello

说明:ld 是 GNU 链接器,它会将你的代码与操作系统的启动规范结合,生成最终的可执行文件。

第三步:运行 (Run)

bash 复制代码
./hello

预期输出:

text 复制代码
Hello, World!

💡 补充说明:如果你使用的是 Windows

Windows 下直接调用系统调用非常复杂(涉及 PE 格式和 Win32 API)。如果你在 Windows 上学习,通常有两种选择:

  1. 使用 WSL (Windows Subsystem for Linux):直接在 WSL 中按照上面的 Linux 步骤操作即可。

  2. 使用 C 运行时库 (CRT) :通过调用 C 语言的 printf 函数来实现。

    nasm 复制代码
    ; win_hello.asm (Windows x64, 需配合 gcc 链接)
    extern printf
    section .data
        msg db "Hello, World!", 10, 0  ; C字符串需要以 0 结尾
    section .text
        global main
    main:
        sub rsp, 40            ; 遵守 Windows x64 调用约定 (Shadow Space)
        lea rcx, [rel msg]     ; 第一个参数:字符串指针
        call printf wrt ..plt  ; 调用 printf
        add rsp, 40
        xor eax, eax           ; return 0
        ret

    Windows 编译命令:

    bash 复制代码
    nasm -f win64 win_hello.asm -o win_hello.o
    gcc win_hello.o -o win_hello.exe
    ./win_hello.exe

二、解释hello.asm的每一行代码

这份 hello.asm 代码是专为 x86-64 Linux 系统编写的,它通过直接调用操作系统的底层接口(系统调用)来实现功能。下面我为你逐行进行详细拆解:

1. 数据段定义 (.section .data)

这一部分用于存放程序运行时需要用到的静态数据。

  • section .data:声明这是一个数据段。在这个段里定义的变量在程序运行期间不会被修改。
  • msg db "Hello, World!", 10 :定义一个名为 msg 的字节数组(字符串)。db 代表 define byte。字符串内容是 Hello, World!,最后的 10 是换行符 \n 的 ASCII 码值。
  • msg_len equ $ - msg :计算字符串的长度并赋值给 msg_len$ 代表当前行的内存地址,$ - msg 就是用当前地址减去字符串起始地址,从而得出字符串占用的总字节数(即长度)。

2. 代码段与入口点 (.section .text)

这一部分包含了程序的执行逻辑和机器指令。

  • section .text:声明这是代码段,专门用来存放汇编指令。
  • global _start :告诉链接器 _start 是一个全局符号。在没有 C 语言运行时库(CRT)的情况下,_start 就是整个程序的绝对入口点,相当于 C 语言中的 main 函数。
  • _start::定义了一个标号(Label),标记了程序开始执行的第一条指令的位置。

3. 输出字符串 (sys_write)

这段代码的作用是向屏幕打印文字。在 x86-64 Linux 中,我们通过 syscall 指令与内核交互,参数需要按照特定的寄存器规则传递。

  • mov rax, 1 :将数字 1 存入 rax 寄存器。在 Linux 64位系统中,1sys_write(写文件/输出)的系统调用号。这告诉操作系统:"我要执行写操作"。
  • mov rdi, 1 :将数字 1 存入 rdi 寄存器。这是 sys_write 的第一个参数,表示文件描述符。1 代表标准输出(stdout,也就是终端屏幕)。
  • mov rsi, msg :将字符串 msg 所在的内存地址存入 rsi 寄存器。这是第二个参数,告诉操作系统去哪里找要输出的内容。
  • mov rdx, msg_len :将之前计算好的字符串长度存入 rdx 寄存器。这是第三个参数,告诉操作系统要输出多少个字节。
  • syscall核心指令 。触发系统调用。此时 CPU 会从用户态切换到内核态,Linux 内核会读取刚才设置好的寄存器,把 msg 的内容输出到屏幕上。

4. 退出程序 (sys_exit)

程序执行完毕后,必须主动通知操作系统释放资源并安全退出,否则会导致进程变成僵尸进程或发生段错误。

  • mov rax, 60 :将数字 60 存入 rax 寄存器。60sys_exit(退出程序)的系统调用号。
  • xor rdi, rdi :对 rdi 寄存器进行异或运算。任何数与自己异或结果都是 0。这是一种非常高效的汇编技巧,用来将 rdi 清零。这里作为 sys_exit 的第一个参数,表示程序的退出状态码为 0(在 Linux 中,返回 0 通常代表程序正常、成功结束)。
  • syscall:再次触发系统调用。内核接收到退出信号后,会清理该程序的内存空间,并将控制权交还给 Shell 终端。

三、介绍用到的寄存器

在 x86-64 架构中,寄存器是 CPU 内部极其高速的存储单元。在我们上面的 hello.asm 代码中,主要用到了 通用寄存器(General-Purpose Registers)

x86-64 架构将传统的 32位寄存器扩展为了 64位,通常以字母 r 开头(例如 eax 变成了 rax)。在 Linux 系统调用(System Call)时,内核有一套严格的传参约定,规定了我们必须把数据放在特定的寄存器里。

以下是我们代码中用到的寄存器的详细拆解:

1. rax (Accumulator Register / 累加器)

  • 角色 :在我们的代码中,它扮演了 "任务调度员" 的角色。
  • 作用 :在执行 syscall 指令之前,rax 必须存放系统调用号 。操作系统就是靠读取 rax 里的数字来决定你要干什么(比如 1 代表写文件,60 代表退出程序)。
  • 补充说明 :当系统调用执行完毕后,rax 还会被用来存放返回值(比如函数返回的结果或错误码)。

2. rdi (Destination Index Register / 目的变址寄存器)

  • 角色第一个参数传递者
  • 作用 :在 Linux x86-64 的系统调用规范中,rdi 永远用来传递函数的第 1 个参数。
    • sys_write 中,它传递的是"文件描述符"(1 代表屏幕)。
    • sys_exit 中,它传递的是"退出状态码"(0 代表成功)。

3. rsi (Source Index Register / 源变址寄存器)

  • 角色第二个参数传递者
  • 作用 :专门用来传递第 2 个参数。在 sys_write 中,它存放的是我们要输出的字符串所在的内存地址 (即 msg 的地址)。

4. rdx (Data Register / 数据寄存器)

  • 角色第三个参数传递者
  • 作用 :专门用来传递第 3 个参数。在 sys_write 中,它存放的是要输出的数据的字节长度 (即 msg_len)。

💡 扩展知识:Linux x86-64 系统调用的完整传参顺序

虽然我们的 Hello World 只用到了前三个参数,但了解完整的规则有助于你以后编写更复杂的汇编程序。如果系统调用需要更多参数,它们会按照以下顺序依次放入寄存器:

参数序号 对应寄存器 记忆提示
系统调用号 rax 决定"做什么操作"
第 1 个参数 rdi Destination Index
第 2 个参数 rsi Source Index
第 3 个参数 rdx Data Register
第 4 个参数 r10 (注意:不是 rcx,因为 syscall 会覆盖 rcx)
第 5 个参数 r8 -
第 6 个参数 r9 -

⚠️ 一个容易混淆的细节:寄存器的大小后缀

你在查阅资料时可能会看到 eax, ax, al 等名称。它们是同一个物理寄存器的不同部分:

  • rax:整个 64位寄存器。
  • eaxrax 的低 32位。
  • axrax 的低 16位。
  • alrax 的低 8位。

(注:在 x86-64 架构下,向低 32位寄存器如 eax 写入数据时,CPU 会自动将整个 64位寄存器的高 32位清零。)

相关推荐
疯狂打码的少年1 天前
【程序语言与编译】程序设计语言分类(机器/汇编/高级)
汇编·笔记
JAMSAN09301 天前
16.0% 高增长!全球异构计算架构服务市场扩容态势
汇编·人工智能·架构
iCxhust3 天前
8086汇编 word ptr
汇编·单片机·嵌入式硬件·微机原理·8088单板机
疏狂难除3 天前
X86-64 Assembly中printf 打印 float 和 double的bug的解决
bug·assembly
大阳1233 天前
ARM.9(RGBLCD,PWM)
c语言·开发语言·汇编·单片机·嵌入式硬件·pwm·rgblcd
2301_789015624 天前
Linux基础开发工具一:软件包管理器、vim编辑器
linux·服务器·c语言·汇编·c++·编辑器·vim
是星辰吖~5 天前
X86反汇编_深度学习_基础二叉树
汇编
iCxhust5 天前
汇编返回指令ret iret retf区别
汇编·微机原理·8088单板机
怣疯knight6 天前
ida里打印python版本
汇编