机器码是如何打印输出“ hello world!“的

来源:互联网 发布:des算法例子 编辑:程序博客网 时间:2024/05/23 11:57

免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!

如果你打算构建自己的操作系统,你将需要熟悉汇编编程,一旦你了解了一个汇编语言,你也许甚至会用它编写一个完整的OS,不论你如何选择,本篇博客将会介绍给你x86—64汇编语言,最接近机器编码语言的语言


准备工作

 在我们开始之前,你需要个一台x86_64的linux机器,并且已经安装nasm程序,我想你可以下载并安装好的。你直接在终端输入如下命令就可以成功安装。

x@x-MacBook:~/asm-tutorial$ sudo apt install nasm 

NASM全称The Netwide Assembler,是一款基于80x86和x86-64平台的汇编语言编译程序


"Hello World!"

  如大多数程序语言的开始教程一样,我们将会以一个最基础的hello world程序开始,我将通过展示代码,并且我建议你手工输入,不要直接复制粘贴,以便更好地记住它,

首先,我们来创建一个目录存储我们的工作文件

$ mkdir asm-tutorial$ cd asm-tutorial$ gedit hello-world.asm

在上面的例子中,我用gedit打开了hello-world.asm,这个好用,通用的文本编辑器,不过,你如果更喜欢emacs,vim或其他的文本编辑器也随意。。

好了,现在我们为我们的hello world程序输入代码,当你已经做完并且成功编译并且运行了以后我将会解释代码是如何工作的。

复制代码
[bits 64]    global _start    section .data    message db "Hello, World!"    section .text_start:    mov rax, 1    mov rdx, 13    mov rsi, message    mov rdi, 1    syscall    mov rax, 60    mov rdi, 0    syscall
复制代码

创建可执行文件

一旦你已经输入完了,保存文件,然后在终端输入下面的指令。

$ nasm -f elf64 hello-world.asm$ ld hello-world.o -o hello-world$ ./hello-worldHello, World!

第一行 nasm -f elf64 hello-world.asm 告诉nasm程序汇编我们的文件,-f elf64则是说明我们想让nasm生成一个elf64格式的目标文件。

 

nasm如我们所知是汇编器,汇编器就是把一个用汇编语言写好的文件,就像我们的hello-world.asm转换成机器码,机器码告诉计算机执行什么操作,nasm生成的文件就叫做目标文件,在我们这个小例子中,nasm产生一个叫做hello-world.o的文件

我们用hexdump工具看看我们的目标文件的内容

复制代码
x@x-MacBook:~/asm-tutorial$ hexdump hello-world.o0000000 457f 464c 0102 0001 0000 0000 0000 00000000010 0001 003e 0001 0000 0000 0000 0000 00000000020 0000 0000 0000 0000 0040 0000 0000 00000000030 0000 0000 0040 0000 0000 0040 0007 00030000040 0000 0000 0000 0000 0000 0000 0000 0000*0000080 0001 0000 0001 0000 0003 0000 0000 00000000090 0000 0000 0000 0000 0200 0000 0000 000000000a0 000d 0000 0000 0000 0000 0000 0000 000000000b0 0004 0000 0000 0000 0000 0000 0000 000000000c0 0007 0000 0001 0000 0006 0000 0000 000000000d0 0000 0000 0000 0000 0210 0000 0000 000000000e0 0027 0000 0000 0000 0000 0000 0000 000000000f0 0010 0000 0000 0000 0000 0000 0000 00000000100 000d 0000 0003 0000 0000 0000 0000 00000000110 0000 0000 0000 0000 0240 0000 0000 00000000120 0032 0000 0000 0000 0000 0000 0000 00000000130 0001 0000 0000 0000 0000 0000 0000 00000000140 0017 0000 0002 0000 0000 0000 0000 00000000150 0000 0000 0000 0000 0280 0000 0000 00000000160 0090 0000 0000 0000 0005 0000 0005 00000000170 0004 0000 0000 0000 0018 0000 0000 00000000180 001f 0000 0003 0000 0000 0000 0000 00000000190 0000 0000 0000 0000 0310 0000 0000 000000001a0 0020 0000 0000 0000 0000 0000 0000 000000001b0 0001 0000 0000 0000 0000 0000 0000 000000001c0 0027 0000 0004 0000 0000 0000 0000 000000001d0 0000 0000 0000 0000 0330 0000 0000 000000001e0 0018 0000 0000 0000 0004 0000 0002 000000001f0 0004 0000 0000 0000 0018 0000 0000 00000000200 6548 6c6c 2c6f 5720 726f 646c 0021 00000000210 01b8 0000 ba00 000d 0000 be48 0000 00000000220 0000 0000 01bf 0000 0f00 b805 003c 00000000230 00bf 0000 0f00 0005 0000 0000 0000 00000000240 2e00 6164 6174 2e00 6574 7478 2e00 68730000250 7473 7472 6261 2e00 7973 746d 6261 2e000000260 7473 7472 6261 2e00 6572 616c 742e 78650000270 0074 0000 0000 0000 0000 0000 0000 00000000280 0000 0000 0000 0000 0000 0000 0000 00000000290 0000 0000 0000 0000 0001 0000 0004 fff100002a0 0000 0000 0000 0000 0000 0000 0000 000000002b0 0000 0000 0003 0001 0000 0000 0000 000000002c0 0000 0000 0000 0000 0000 0000 0003 000200002d0 0000 0000 0000 0000 0000 0000 0000 000000002e0 0011 0000 0000 0001 0000 0000 0000 000000002f0 0000 0000 0000 0000 0019 0000 0010 00020000300 0000 0000 0000 0000 0000 0000 0000 00000000310 6800 6c65 6f6c 772d 726f 646c 612e 6d730000320 6d00 7365 6173 6567 5f00 7473 7261 00740000330 000c 0000 0000 0000 0001 0000 0002 00000000340 0000 0000 0000 0000 0000 0000 0000 00000000350
复制代码

看到了吧,机器码果断是为机器看的,而不是为人看的。

让这个程序运行起来的最后一步就是linking,链接了,它由系统链接器完成,在linux上,这个工具叫做ld,linking就是把目标文件组合,转换成可执行文件。

-o hello-world 这个选项就是告诉ld我们想要生成的可执行文件名为hello-world。

最后我们通过在我们的文件名之前加"./"来运行我们的程序,程序返回Hello, World!


CPU指令的执行过程
在我们开始深入挖掘分析我们的程序之前,了解一下CPU如果执行是很有好处的,一般来说,CPU的目的是执行一个有意义的指令序列,通常分为四步,取指,译码,执行,写回

1.取指
   第一步,取指,包含从程序内存中取出一条指令,指令在内存中的位置由程序计数器(PC)指定,一旦一条指令取到,程序计数器就会指向下一条指令。

2.译码

   译码,确定了CPU将会执行什么操作,指令通常分为两部分,操作码,指定了执行的操作,剩下的那部分通过提供了请求执行该操作的信息,可能是常量,寄存器或者内存地址,(操作数引用)

3.执行

   上一步完成后,开始进入执行步,CPU的各个部分相互联系以使他们可以执行由操作码指定的操作。然后操作执行

4.写回

   最后一步,写回,仅仅是把执行的结果写到一种存储地址中,比如寄存器或是内存地址,并不是所有的指令都有输出值,有些指令操作程序计数器,这些地址叫做跳转指令,jump指令使得循环,,条件语句和函数调用变得简单。

理解寄存器

 寄存器就是cpu内部的小容量的存储器,我们关心的主要是三类寄存器,数据寄存器,地址寄存器和通用寄存器。

  • 数据寄存器保存数值,(比如整数和浮点值)
  • 地址寄存器,保存内存中的地址
  • 通用寄存器,既可以用过数据寄存器也可以用做地址寄存器。

汇编程序员大部分工作都是在操作这些寄存器。

分析我们的源代码

 有了上面的背景知识,我们现在可是分析我们的代码,我将把程序分成很多小段,并解释每步完成了什么操作。

[bits 64]

我们程序的第一行是一个汇编指令,如你所猜的,这是告诉nasm我们想要得到可以运行在64位处理器上的代码。

 global _start

这一行是另一条汇编指令,告诉nasm 用_start 标记的代码段(section)应该被看成全局的,全局的部分通常允许其他的目标文件引用它,在我们的这个例子中,我们把_start 段标记为全局的,以便让链接器知道我们的程序从哪里开始。

 section .data message db "Hello, World!"

上面代码的第一行又是一条汇编指令,告诉nasm后面跟着的代码是data段,data段包含全局和静态变量。

下一句,我们有这样的静态变量。message db "Hello, World!"db被用来声明初始化数据,message是一个变量名,与"Hello, World!"关联。

  section .text

上面这句是另一个段指令section,但是这次它是告诉nasm把紧跟着的代码存到text段,text段有时候也叫做code段,它是包含可执行代码的目标文件的一部分。

最后,我们要到这个程序最重要的部分了。

复制代码
_start:    mov rax, 1    mov rdx, 13    mov rsi, message    mov rdi, 1    syscall    mov rax, 60    mov rdi, 0    syscall
复制代码

第一行。_start:,把它后面的代码和_start标记相关联起来。

    mov rax, 1    mov rdx, 13    mov rsi, message    mov rdi, 1

上面这四行都是加载值到不同的寄存器里,RAX和RDX都是通用寄存器,我们使用他们分别保存1和13,RSI和RDI是源和目标数据索引寄存器,我们设置源寄存器RSI指向message,而目标寄存器指向1。

现在,当寄存器加载完毕后,我们有syscall指令,这是告诉计算机我们想要使用我们已经加载到寄存器的值执行一次系统调用,我们加载的第一个数也就是RAX寄存器的值,告诉计算机我们想使用哪一个系统调用,syscalls表和其对应的数字可以查询这里

 

就像你从那张表里看到的。RAX里的1意味着我们想要调用write(int fd, const void* buf, size_t bytes). 下一条指令 mov rdx,13,则是我们想要调用的函数write()的最后一个参数值,最后一个参数size_t bytes指令了message的大小,此处 13也就是"Hello, World!"的长度,

 

下两条指令 mov rsi, messagemov rdi,1则分别作了其他两个参数,因此,当我们把他们放在一起,当要执行syscall的时候,我们是告诉计算机执行write(1, message, 13),1就是标准输出stdout,因此本质上,我们是告诉计算机从message里取13个字节输出到stdout。

    mov rax, 60    mov rdi, 0    syscall

现在,可能你想知道我们已经执行完了所有的功能,为什么后面还有个syscall,你不用猜了,直接去查前面所说的那个表 我们知道了,60引用exit().因为,syscall其实就是调用exit(0)了。

总结一下

所以啊,我们汇编程序的第一部分就是把message变量和"Hello, World!"对应起来,然后,程序的关键部分是写两个syscall,一个是write(),另一个是exit(),把他们放在一起,如果你想把汇编程序翻译成c语言的话,大概看起来会是这个样子。

复制代码
int main() {    char* message = "Hello, World!"        write(1, message, 13);    exit(0);}
复制代码


著作权声明:本文由http://www.cnblogs.com/lazycoding翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!

                                                                                   好东西就应该分享!