汇编系列02-借助操作系统输出Hello World
说明:本节的程序使用的是x86_64指令集的。
汇编语言是可以编译成机器指令的,机器指令是可以直接在CPU上面执行的。我们编写的汇编程序既可以直接在操作系统的帮助下执行,也可以绕过操作系统,直接在硬件上执行。
如果你打算编写的汇编程序在操作系统帮助下执行,那么你的汇编可以调用很多操作系统暴露的系统调用,或者别的语言的函数库,比如C语言的标准库中的函数。但是如果你打算编写绕过操作系统
执行的汇编,会比较累,比如往显示器上显示一串字符串这件事情,如果你打算让操作系统帮你执行,你可以在汇编程序中直接调用系统调用(这一节我们打算这么做),来控制硬件(操作系统能帮助我们控制硬件),如果你打算自己去控制硬件,就比较麻烦点(下一节我们打算这么做)。
Hello World汇编文件编写
hello.asm文件的内容(等一会再详细剖析代码):
;hello.asm 交给nasm汇编器汇编的程序,通过英文分号开头表示注释
section .datamsg db "Hello, wolrd",0,0xA ;db表示define byte,即字节类型,逗号后边的0表示字符串结尾;字符串中的每个字符占用一个字节,0xA是十六进制数,对应的换行符的ASCII码;这样后边输出字符串的时候,就可以还行了
section .bss
section .textglobal main
main:mov rax,1 ;为了后续使用syscall,先将系统调用号1(含义是write)存到寄存器rax中mov rdi,1 ;rdi寄存器通常表示目的地,1表示标准输出mov rsi,msg ;rsi寄存器通常表示来源地,需要显示的是字符串,存放在rsi中mov rdx,14 ;字符串的长度和末尾的0及换行符的长度。注意,汇编中的数字,如果不加0x前缀或者h后缀,表示的是十进制syscall ;发起x86_64的系统调用,显示字符串mov rax,60 ;程序的返回结果放在rax寄存器中,60表示退出,如果为负数表示发生了错误mov rdi,0 ;0表示成功退出码syscall ;退出
hello.asm同一个目录下的makefile文件内容:
#hello.asm的makefile makefile的注释用#开头
hello: hello.o
# no-pie选项是为了关闭pie,从而生成可调试的可执行文件
# pie是position independent executable的意思,表示与地址无关的
# 如果编译器在生成可执行程序的过程中使用了pie,那么当可执行程序被加载到内存中时,其加载地址存在不可预知性gcc -o hello hello.o -no-pie
hello.o: hello.asm
# nasm -f 后边表示的是输出的格式
# elf64表示的是64位可执行和可链接的格式nasm -f elf64 hello.asm
编译、执行
pilaf@pilaf-thinkpad:~/assembly_x64/hello_asm$ pwd
/home/pilaf/assembly_x64/hello_asm
pilaf@pilaf-thinkpad:~/assembly_x64/hello_asm$ ls
hello.asm makefile
pilaf@pilaf-thinkpad:~/assembly_x64/hello_asm$ make
nasm -f elf64 hello.asm
gcc -o hello hello.o -no-pie
pilaf@pilaf-thinkpad:~/assembly_x64/hello_asm$ ls
hello hello.asm hello.o makefile
pilaf@pilaf-thinkpad:~/assembly_x64/hello_asm$ ./hello
Hello, wolrd
pilaf@pilaf-thinkpad:~/assembly_x64/hello_asm$
make命令后生成了一个可执行的hello文件,然后执行它,在shell中输出了Hello, world。
反编译可执行文件
下面我们通过gdb看一下hello可执行程序反汇编的结果:
dissassemble/r main
,可以显示出main函数(global main定义)的十六进制指令及其汇编指令关系,最左边的蓝色的一列是二进制可执行程序的内存地址
(逻辑地址)。可以看到syscall指令的机器码是0f05。
上边很有意思的是这几行:
0x0000000000401110 <+0>: b8 01 00 00 00 mov eax,0x10x0000000000401115 <+5>: bf 01 00 00 00 mov edi,0x10x000000000040111a <+10>: 48 be 28 40 40 00 00 00 00 00 movabs rsi,0x4040280x0000000000401124 <+20>: ba 0e 00 00 00 mov edx,0xe0x0000000000401129 <+25>: 0f 05 syscall 0x000000000040112b <+27>: b8 3c 00 00 00 mov eax,0x3c0x0000000000401130 <+32>: bf 00 00 00 00 mov edi,0x0
内存地址后边跟着的尖括号中的"+数字"表示相对第一个指令的内存地址的偏移字节数
。
第二条指令mov edi,0x1
相对于第一条指令mov eax,0x1
偏移了5个字节,正好就是b8 01 00 00 00
这5个字节所占的内存长度。而mov edi,0x1
也占用了5个字节的内存,所以它后便的内存中的指令movabs rsi,0x404028
相对于它又增加了5个字节(10-5=5)。
还有我们发现有mov eax
的地方,机器码都是b8开头,有mov edi
的地方,机器码都是bf开头,后便跟着4个字节,就是mov指令有操作数。这也就是说了mov eax,mov edi在指令中一共才占用1个字节,这1个字节就表示了mov和寄存器两个信息了
。
我们还可以发现,b8、bf后边跟着的4个字节(32位整数的十六进制表示),和mov指令右操作数好像不一样啊,其实这是因为Intel CPU采用的是小端字节序
,对应的还有大端字节序
。所谓的大端还是小端,指的是将一个多字节的数存放到内存的时候,这个数字的权重大
的字节(比如,十进制的102这个数字的1的权重比2大)放在内存低地址还是权重小
的字节放在内存低地址。如果权重低的字节放在内存低地址,就是小端字节序,如果权重大的字节放在内存低地址就是大端字节序
。
我们的0x01是0x00000001这个四字节数的简写,但是却把01这个字节放在内存低地址,所以它是小端字节序
。
等等,不知道你有没有发现一个问题,我们hello.asm中用的不是rax
么?怎么反编译完了变成了eax
了?好多r开头的寄存器怎么都变成e开头的了?这是因为r开头的寄存器是64位的,e开头的是32位的,汇编器发现mov右操作数没有必要用64位寄存器存储,就优化了。不知你是否还记得,8086的几个通用寄存器,ax、bx、cx、dx(它们又可以拆分为两个8位寄存器,如ah、al,h表示high,l表示low),80386开始有eax、ebx、ecx、edx,e表示extend扩展的意思,它们都是32位的寄存器了,到了64位,有了rax、rbx、rcx、rdx这些寄存器。类似的还有ip、eip、rip表示存放下一个指令内存地址的寄存器。
系统调用方式之syscall
好了,让我们再来看看syscall这个汇编指令
。
书上说,进行系统调用的可以通过下面三种方式:
- INT(中断Interrupt,旧的方式,不太推荐用了)
- syscall指令(syscall是汇编指令,我们hello.asm中用到了)
- 程序库API(如C语言的标准库中的printf就对write系统调用进行了封装)
作为菜鸟的我,看到《x64汇编语言:从新手到AVX专家》中给出的这段汇编,有很多地方不明白的,比如mov rax,1
和mov rdi,1
中的1有什么联系?为什么要往rax中mov,不往别的寄存器mov?
带着这些疑问,我man了一下syscall(Linux shell中执行了man syscall
):
内容很长,我只截了一部分。从中我们可以看到,C头文件中也定义了一个syscall的函数(名字和汇编的syscall指令相同),它是一个小的库函数,它用于调用一些系统调用,这些系统调用(系统调用是操作系统对硬件操作的封装,对硬件操作少不了汇编指令,Linux操作系统的系统调用的实现都是C语言中嵌套着汇编
)的汇编接口有特定的号码和参数。syscall函数可以调用其它的系统调用,只需要按照调用约定
将系统调用号和参数传递给它即可。
也就是说,汇编提供了一个syscall指令用于调用操作系统的系统调用,只需要将操作系统的系统调用号和参数准备好就可以了。
我们可以通过汇编直接调用syscall指令,也可以用C语言提供的syscall函数(这个函数内部实现肯定是包了汇编指令syscall的)。
并且系统调用号的数字和符号表示关系在<sys/syscall.h>
中有定义。
syscall汇编指令的用法
我继续阅读man syscall
给出的信息,发现下面这些有用的信息。
表一
:
文档直接给出了x86-64
的syscall指令
的使用方法,也就是说调用syscall之前,需要将系统调用号(System call #,英文中经常用井号表示Number的缩写)放到rax寄存器,返回值1放到rax中,返回值2放到rdx中。
还有下面的入参信息。
表二
:
说明了x86-64的系统调用的参数1放在rdi寄存器中,参数2放在rsi寄存器中,参数3放在rdx寄存器中,参数4放在r10寄存器中,参数5放在r8寄存器中,参数6放在r9寄存器中
。
回到我们的hello.asm,我们用了汇编的syscall指令
调用了操作系统的系统调用,按照上面截图中说的x86-64
使用syscall汇编指令
的时候,会将操作系统的系统调用号放到rax寄存器中,我们的程序写的是mov rax,1
,也就是说系统调用号是1,我们刚才看到说系统调用号和它的符号表示在<sys/syscall.h>
头文件中的,我们在CLion(Jetbrains全家桶中的C语言集成开发环境IDE)中随便打开一个c文件,引入头文件:
一路追踪下来,终于发现,操作系统调用号为1的系统调用名称是write
!废话不多说,直接执行man 2 write
看看:
可以看到Linux操作系统的write系统调用的参数有三个,第一个是文件描述符(Linux中一切皆文件,0表示标准输入,1表示的标准输出文件描述符,2表示错误输出
),第二个参数是一个指针,即输入内容起始内存地址,第三个参数表示输入的字节数。
上面表二中说了,Linux操作系统的系统调用的入参,第一个放到rdi寄存器,第二个放到rsi寄存器,第三个放到rdx寄存器等等。结合man 2 write
得到的知识,传递给write系统调用
的三个参数分别是1,msg的地址,14(Hello, world字符及其结尾0加上换行一共14字节)。
也就是说我们的hello.asm干了这么件事情:通过汇编的syscall指令,调用了Linux系统调用write,让write往标准输出上面输出14个字节,内容是"Hello, world"然后换行。
总结
作为菜鸟的我在运行书中的一段hello world程序的时候,书中很多地方没有写明为什么这样做,笔者通过一点点摸索,终于搞清楚其底层的运行逻辑,顿感欢喜,以此记录,希望能帮到和我一样菜的菜鸟。也希望大家不要浮躁,沉下心来,夯实基础,做一个有工匠精神的码农,也许某一天平时积累的点滴就派上了大用场。加油!
参考资料
1.《x64汇编语言:从新手到AVX专家》
2.《汇编程序设计与计算机体系结构 软件工程师教程》P211
相关文章:

汇编系列02-借助操作系统输出Hello World
说明:本节的程序使用的是x86_64指令集的。 汇编语言是可以编译成机器指令的,机器指令是可以直接在CPU上面执行的。我们编写的汇编程序既可以直接在操作系统的帮助下执行,也可以绕过操作系统,直接在硬件上执行。 如果你打算编写的汇编程序在…...

【2023unity游戏制作-mango的冒险】-前六章API,细节,BUG总结小结
👨💻个人主页:元宇宙-秩沅 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 本文由 秩沅 原创 收录于专栏:unity游戏制作 ⭐mango的冒险前六章总结⭐ 文章目录⭐mango的冒险前六章总结⭐👨&a…...

进程控制及其操作
进程创建1.1 fork()函数1.2 fork()函数的返回值进程等待2.1 进程等待的必要性1.之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。 2.另外,进程一旦变成僵尸状态,那…...

Git常用命令复习笔记
1. Git与SVN区别,各自优缺点 Git: 分布式,每个参与开发的人的电脑上都有一个完整的仓库,不担心硬盘出问题;在不联网的情况下,照样可以提交到本地仓库,可以查看以往的所有log,等到有…...

代码随想录算法训练营day49 | 动态规划 123.买卖股票的最佳时机III 188.买卖股票的最佳时机IV
day49123.买卖股票的最佳时机III1.确定dp数组以及下标的含义2.确定递推公式3.dp数组如何初始化4.确定遍历顺序5.举例推导dp数组188.买卖股票的最佳时机IV1.确定dp数组以及下标的含义2.确定递推公式4.dp数组如何初始化4.确定遍历顺序5.举例推导dp数组123.买卖股票的最佳时机III …...

【教学典型案例】14.课程推送页面整理-增加定时功能
目录一:背景介绍1、代码可读性差,结构混乱2、逻辑边界不清晰,封装意识缺乏3、展示效果不美观二:案例问题分析以及解决过程1、代码可读性…...

【算法基础】DFS BFS 进阶训练
DFS与BFS的基础篇详见:https://blog.csdn.net/m0_51339444/article/details/129301451?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22129301451%22%2C%22source%22%3A%22m0_51339444%22%7D 一、案例分析1 (树的重心 —— D…...

GO语言中的回调函数
0.前言 回调函数是一种在编程中常见的技术,通常在异步编程中使用。简单来说,回调函数是一个被传递给另一个函数的函数,它在该函数的某个时间点被调用,以完成某些特定的操作或任务。 在Go语言中,可以将函数直接作为参…...

28个案例问题分析---014课程推送页面逻辑整理--vue
一:背景介绍 项目开发过程中,前端出现以下几类问题: 代码结构混乱代码逻辑不清晰页面细节问题 二:问题分析 代码结构混乱问题 <template><top/><div style"position: absolute;top: 10px"><C…...

佛科院单片机原理2——80C51单片机结构
一、程序存储器的入口地址:程序入口地址:0000H外部中断0入口地址:0003H定时器0溢出中断入口地址:000BH外部中断1入口地址:00013H定时器1溢出中断入口地址:001BH串行口中断入口地址:0023H定时器2…...

数据结构与算法_动态顺序表
顺序表是线性表的一种。 线性表是n个具有相同特性的数据元素的有限序列。 逻辑上,它们是线性结构,是一条连续的直线;但是在物理上,它们通常以数组和链式结构存储。 常见的线性表有顺序表、栈、队列、字符串等。 顺序表是用一段…...

逃避浏览器JS检测打开开发者工具
20230304 - 0. 引言 看到一些视频网站之后,想把视频离线下载下来怎么办?直接的方法是通过开发者工具来查看网络流量,一般可以在传输的请求类型中搜索m3u8,然后找到这部分请求,然后利用某些播放器或者下载器直接下载。…...

ceph介绍、原理、架构、算法...个人学习记录
前言 之前公司安排出差支援非结构化项目,采用springcloud(redismysql数据冷热处理)s3escephkafka还涉及一些区块链技术等等…,在与大佬的沟通交流下对ceph产生了兴趣,私下学习记录一下;后续工作之余会采用上面相关技术栈手动实现不…...

Spring MVC源码解析——HandlerMapping(处理器映射器)
Sping MVC 源码解析——HandlerMapping处理器映射器1. 什么是HandlerMapping2. HandlerMapping2.1 HandlerMapping初始化2.2 getHandler解析3. getHandlerInternal()子类实现3.1 AbstractUrlHandlerMapping与AbstractHandlerMethodMapping的区别3.2 AbstractUrlHandlerMapping3…...

【Word/word2007】将标题第1章改成第一章
问题:设置多级列表没有其他格式选的解决办法和带来的插入图注解的问题,将标题第1章改成第一章的问题其他方案。 按照百度搜索的方法设置第一章,可以是没有相应的样式可以选。 那就换到编号选项 设置新的编号值 先选是 然就是变得很丑 这时打开…...

NLP预训练模型
Models Corpus RoBERTa: A Robustly Optimized BERT Pretraining Approach 与BERT主要区别在于: large mini-batches 保持总训练tokens数一致,使用更大的学习率、更大的batch size,adam β20.98\beta_20.98β20.98;dynamic ma…...

Typora上传文档图片链接失效的问题+PicGo布置图床在Github
文章目录typora图片链接失效原因PicGO开源图床布置先配置Github2.1先创建新仓库、用于存放图片2.2生成一个token,用picGo访问github3.下载picGo,并进行配置3.1 配置v4.1typora图片链接失效原因 因为你是保存在本地的,因此图片是不能访问,可以…...

win10安装oracle
文件放到最后。我的电脑是win11的,因为老师让写下安装笔记,在11上安装的时候没有截屏,所以在虚拟机上重新安装下吧。室友说要把文件夹放到c盘才能打开。我试了下,具体的是要把Oracle11g文件夹放到c盘根目录下。如果解压后不是这个…...

AQS为什么用双向链表?
首先,在AQS中,等待队列是通过Node类来表示的,每个Node节点包含了等待线程的信息以及等待状态。下面是Node类的部分源码:static final class Node {// 等待状态volatile int waitStatus;// 前驱节点volatile Node prev;// 后继节点…...

AtCoder Beginner Contest 292——A-E题讲解
蒟蒻来讲题,还望大家喜。若哪有问题,大家尽可提! Hello, 大家好哇!本初中生蒟蒻讲解一下AtCoder Beginner Contest 292这场比赛的A-E题! A题 原题 Problem Statement You are given a string SSS consisting of lo…...

(蓝桥真题)最长不下降子序列(权值线段树)
样例输入: 5 1 1 4 2 8 5 样例输出: 4 分析:看到这种对其中连续k个数进行修改的我们就应该想到答案是由三部分组成,因为求的是最长不下降子序列,那么我们可以找到一个最合适的断点i,使得答案是由区间[1…...

数据类型及参数传递
1.数据类型 java中的基本数据类型: 数值型: 整数型:byte short long int 浮点型:float double 布尔型: boolean字符串: char java中的引用数据类型: 数组(array) 类(class…...

永春堂1300系统开发|解析永春堂1300模式商城的五大奖项
电商平台竞争越来越激烈,各种营销方式也是层出不穷,其中永春堂1300营销模式,以其无泡沫和自驱动性强等特点风靡一时。在这套模式中,虽然单型价格差异较大,但各种奖励的设计,巧妙的兼顾了平台和所有会员的利…...

最近一年我都干了什么——反思!!
过去一年不管是学习方式还是心态上都和以往有了许多不同的地方,比较昏昏沉沉。最近慢慢找到状态了,就想赶紧记录下来。 学习 在学习新技术的过程中开始飘了,总感觉有了一些开发经验后就觉得什么都不用记,知道思路就行遇到了现场百…...

Docker学习(十七)save 和 export 命令的区别
Docker 中有两个命令可以将镜像导出为本地文件系统中的 tar 文件:docker save 和 docker export。尽管它们的作用类似,但它们之间有一个重要的区别。 1.使用方式的不同: docker save 的使用示例: docker save -o test.tar image…...

【数据结构初阶】详解“树”
目录 前言 1.树概念及结构 (1)树的概念 (2)树的名词介绍 (3)树的表示 编辑 2.二叉树概念及结构 (1)概念 (2)特殊的二叉树 (3࿰…...

20230304 CF855 div3 vp
Dashboard - Codeforces Round 855 (Div. 3) - Codeforces呃呃,评价是,毫无进步呃呃呃呃呃呃呃呃呃呃呃呃呃呃呃呃呃该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训…...

UML 时序图
时序图(Sequence Diagram)是显示对象之间交互的图,是按时间顺序排列的。 时序图中显示的是参与交互的对象及其对象之间消息交互的顺序。 时序图包括的建模元素主要有:对象(Actor)、生命线(Lif…...

详解进程 及 探查进程
进程的概念PCB是什么task_struct的作用如何执行进程进程的探查什么是bashps命令的使用(查看进程)创建进程探究父子进程进程的概念 简而言之,进程就是正在在执行的程序 之前说过,程序执行的第一步Windows是双击程序Linux是 ./ &a…...
汇编相关问题
汇编语言期末复习题DX:单项选择题 DU:多项选择题 TK:填空题 MC:名词解释 v JD:简答题 CXFX:程序分析题 CXTK:程序填空题 BC:编程题第1章:基础知识1、在汇编语言程序的开发…...