将文件保存为test.com文件,恭喜你,你刚刚完成了一个伟大"壮举",你成功的让CPU计算出了1+1等于几,如果你兴匆匆的运行它,什么结果都看不到,那是因为为了保证代码简单,还没有告诉CPU输出结果的缘故,你愿意的话,可以运行cmd,切换到保存test.com的目录,通过执行debug test.com,来看看我们到底输入了什么
可以看出,我们的代码对应了两条机器指令,每个指令分成两个部分,比如MOV ax,1的二进制代码,1011 1000 代表 MOV ax他指定了本条指令的操作,叫做指令操作码(Opcode),0000 0001 0000 0000 代表1,指定了操作的操作数,可以看出机器码是有自己固定的格式,只要掌握了这个格式,查询对应的操作码,应该就可以掌握机器语言了
当然,事情也有复杂的一面,同一条汇编指令其操作码可能根据寻址方式或寄存器或操作数的位数的变化发生变化,比如同样是MOV指令,MOV al,1 和MOV ax,1中Mov的操作码分别为B0(1011 0000)和B8(1011 1000),而MOV ax,[ES:100]操作码会变成26 A1(前面26是段超越前缀,现在不用仔细追究),Intel8086规定的MOV指令就有12种之多,而且操作码的长度还有可能不同,这些操作码都可以在表<>中对应的查到,不需要记忆,下面我们就来了解机器语言指令的格式
在阅读Intel公司的实现前,为了不让您陷入一堆的解释和说明中迷惘无助,我们先来热热身,做点有趣的事情---思考一下如果让你自己来设计机器语言指令的格式,那么你会做出怎样的设计,下面是我的设计思路
111 DI
然后我们再列一个指令码表,比如
MOV=00000000
ADD=00000001
AND=00000010
.
.
.
则MOV ax,1就可以变成 00000000 00000000 00000001(ax是000)
但是这样简单清晰的三个部分会出现一些问题mov bx,0,和mov bx,ax就有可能混淆了,因为ax的代码是000,和立即数0相同
所以我们需要一个标志位来确定是那种操作数,操作数有下面5种可能
目的操作数和原操作数的大小就比较难了,因为操作数可能是
1)一个立即数 比如1
2)一个寄存器 ax,bx,cx,dx
3)一个内存地址 [StringLable]
4)一个由一个或多个寄存器组成的内存地址
[ebx],[ebx+esi],[es:ebx+esi]
5)一个由一个或多个寄存器再加上一个偏移量组成的内存地址
[ebx+esi]
显然我们需要两个标志字段,每个5个值,(每个操作数一个)来标定自己是哪种操作数,每个标志字段只要3位就够了,我把这两个标志字段放到一个字节里,放在两个操作数前面
格式一:
|
指令码 |
保留2位|标志1|标志2| |
操作数1 |
操作数2 |
Mov ax,1 |
00000000 |
00|001|000 |
00000000 |
00000001 |
标志的意义
000:立即数
001:寄存器
010:内存地址
011:多个寄存器
100:多个寄存器加偏移量
问题又出来了,当标志位为100,这时,操作数应该是多个寄存器+偏移量,假设每个寄存器占3位,两个就是6位,留给我们的偏移量的空间只有两位,也就是说偏移量最大只有3,这显然是不够的,所以我们必须加上一个字节表示偏移量,而当不需要偏移量的时候,这两个字段可以不存在,也就是说表格变成了
格式二:
|
指令码 |
00|标志1|标志2 |
操作数1
|
偏移量 |
00|操作数2
bbb|iii |
偏移量 |
Mov ax,[bp+si+5] |
00000000 |
00|001|100 |
00000000 |
|
00|101|110 |
00000110 |
怎么样,有点像样子了吧,固定长度8位的指令码可能有256种指令,我想最基本的操作,AND,OR,XOR,ADD,SHR,SHL等等不会太多,而其他的操作都可以由这些操作组合而成,比如减法是补码的加法,乘法是重复相加等
似乎大部分问题都已经解决了,但是稍微熟悉x86汇编的朋友就会知道,不可能有任何指令的两个操作数都是内存,也就是永远不会出现
MOV [dx+di],[ex+si]这样的语句,要想实现这样的移动我们必须要把源操作数移动到一个寄存器里,然后再从寄存器里移动到目的地
反应在我们的设计上,我们就会发现两个偏移量是多余的,任何情况下最多会有一个被使用到,所以表格可以修改成这样
格式三:
|
指令码 |
00|标志1|标志2 |
偏移量 |
操作数1
|
操作数2
00|bbb|iii |
MOV ax,[bp+si+5] |
00000000 |
00|001|100 |
00000110 |
00000000 |
00|101|110 |
MOV ax,bx |
00000000 |
00|001|001 |
无 |
00000000 |
00000011 |
其实看看上表的第二条语句,我们就会发现一个很重大的问题,那就是空间浪费,第二行中所有黑体的部分都是被浪费掉的空间,浪费了12位,总共才32位的代码,居然就浪费了12位,心疼啊,而且看看标志字段,占了三位,总共可以表示8个标志,确只用了5个,我们能不能想办法把这些空间利用起来呢?
我们重新仔细考虑第二个字节,也就是标志字节,把最高位的两位利用起来,称作寄存器标志,他的值如下表
00:操作数中没有寄存器
01:操作数的后一个为寄存器
10:操作数的前一个为寄存器
11:两个操作数都是寄存器
如果此位指明某操作数为寄存器,则后面的标志位直接为寄存器值,如果为00,则后面的操作数只可能为 (内存,立即数) 形式,这样MOV ax,bx的机器码就变成了下面的样子
格式四:
|
指令码 |
寄存器标志|标志1|标志2 |
偏移量 |
操作数1
|
操作数2
00|bbb|iii |
MOV ax,bx |
00000000 |
11|000|011 |
无 |
无 |
无 |
好了,指令系统的雏形已经出来了,虽然和Intel的实现有很多不同,并且本身还有各种问题,比如依然有浪费空间的情况,功能也不太健全,不过基本体现了指令格式的特点:
- 分成几个字段表示不同意义
- 尽量短小精干
- 不能浪费任何一位
下面让我们来看看Intel公司的实现方法
让书写机器码像填表一样简单
从上面的叙述,我们已经大概能看出点门道,每条指令分为几个部分,表示不同的含义.Intel规定,机器指令都可以被表示成六个部分,Prefix,Opcode,ModR/M,SIB,Displacement,Immediate,除了Opcode部分是必须的外,其他部分都有可能不存在
好像有点复杂不是?不要着急,我们稍作解释就可以把书写机器指令变得像填写表格一样简单
下面我们把几条命令按照六个部分进行分割,填写到这张表里,后面会解释六个部分的含义
|
Prefix
前缀
0-4个前缀,每个1字节
可选 |
Opcode
操作码
1-2字节
一定存在 |
ModR/M
寻址与寄存器
1个字节
可选 |
SIB
内存寻址模式
一个字节
可选 |
Displayment
偏移量
1,2或4个字节
可选 |
Immeidate
立即数
1,2或4个字节
可选 |
|
|
|
oo|rrr|mmm |
cc|iii|bbb |
|
|
MOV ax,1 |
无 |
1011 1000 |
无 |
无 |
无 |
0001 0000 |
ADD ax,1 |
无 |
0000 0101 |
无 |
无 |
无 |
0001 0000 |
MOV ax,[ES:0100h] |
0010 0110(26h代表es的段超越前缀) |
1010 0001 |
无 |
无 |
无 |
0000 0000
0001 0000 |
mov ax,[ebx+esi*2+1] |
0110 0111
(67h,代表使用了32位 |
1000 1011 |
01 000 100 |
01 110 011 |
0000 0001 |
无 |
mov [ebx+esi*2+1],01h |
67 |
1100 0111 |
01 000 100 |
01 110 011 |
0000 0001 |
0000 00001 |
只要会填这个表,我们就可以写出所有的机器代码.
可以看到,Intel的格式中并没有明确的标出两个操作数,而是把偏移量和立即数单独拿了出来,而且同一条指令的操作码会根据寻址方式的不同而变化,不像我们的设计,MOV就是MOV,所有的MOV指令都对应同样的操作码,Prefix部分也是我们的设计所没有的
下面简单的解释下这六个部分,每个部分的具体含义和使用,后面的例子里会逐步阐述
prefix:
指令前缀,为了一些特殊的定义或者操作而存在,只有10个可能的值,可以在下表里面查到,我们大致了解下就是了
• 锁(Lock)和重复前缀:
锁前缀用于多CPU环境中对共享存储的排他访问。重复前缀用于字符串的重复操作,他可以获得比软件循环方法更快的速度。
— F0H—LOCK 前缀.
— F2H—REPNE/REPNZ 前缀.
— F3H—REP 前缀
— F3H—REPE/REPZ prefix (used only with string instructions).
• Segment override:
根据指令的定义和程序的上下文,一条指令所使用的段寄存器名称可以不出现在指令格式中,这称为段缺省规则。当要求一条指令不按缺省规则使用某个段寄存器时,必须以段取代前缀明确指明此段寄存器。
— 2EH—CS 段前缀
— 36H—SS 段前缀.
— 3EH—DS 段前缀.
— 26H—ES 段前缀.
— 64H—FS 段前缀.
— 65H—GS 段前缀.
• 操作大小前缀 66H 和 地址长度前缀 67H
Opcode:
操作码,这个操作码指定了具体的操作,他的值可以在下表查到,注意查表时候要根据操作类型,操作数类型和寻址方式来查询,比如Mov指令有12种操作操作码,我们需要根据操作数的类型,比如Mov bx,1,的两个操作数一个是寄存器,一个是立即数,即Reg,Imm,查下表,应为1011wrrr
MemOfs,Acc 1010001w
Acc,MemOfs 1010000w
Reg,Imm 1011wrrr
Mem,Imm 1100011woo000mmm
Reg,Reg 1000101woorrrmmm
Reg,Mem 1000101woorrrmmm
Mem,Reg 1000100woorrrmmm
Reg16,Seg 10001100oosssmmm
Seg,Reg16 10001110oosssmmm
Mem16,Seg 10001100oosssmmm
Seg,Mem16 10001110oosssmmm
Reg32,CRn 000011110010000011sssrrr
CRn,Reg32 000011110010001011sssrrr
Reg32,DRn 000011110010000111sssrrr
DRn,Reg32 000011110010001111sssrrr
Reg32,TRn 000011110010010011sssrrr
TRn,Reg32 000011110010011011sssrrr
表中rrr,w,mmm,oo都可以看做几个变量, 会根据寄存器,和寻址方式的变化而变化,如果使用4位寄存器,比如al,ah,bl,bh等,则其值为0,否则为1,表<>可以查到,注意所查的结果中已经包含了后面的ModR/M字节
ModR/M和SIB:
这两个字节共同决定了寻址方式,ModR/M包含三个部分oo|rrr|mmm:这三个部分联合表示了寻址方式,oo指示了寻址模式,rrr:指明所用寄存器,注意使用<>查询得到的结果里已经包含ModR/M字节,而SIB是辅助的寻址方式确定位,也包含三个部分
- ss:放大倍数
- iii:变址寄存器
- bbb:基址寄存器
比如如果用到这样的地址[ebp+5*esi],则ebp为基址寄存器,esi为变址寄存器,5为放大倍数
Displayment偏移量位:寻址方式中的偏移量,如[ebp+5]中的5
Immediate:立即数,操作数中的立即数
一起练练手:人肉翻译汇编代码
一) mov bx,cx
查询其操作码为1000 100w,由于使用16位寄存器,则w=1 得到100010001即16进制的89H
ModR/M 包含三个部分oo|rrr|mmm:这三个部分联合表示了寻址方式,这里由于没有内存寻址,查表得,oo=11,rrr和mmm各表示一个寄存器,那么问题来了:哪个表示目的寄存器bx,哪个表示源寄存器cx呢?翻文档太累了,不如用nasm汇编一下这条指令瞧瞧.得到的ModR/M字节为对应寄存器代码可以看出来,rrr表示的是源寄存器bx,则这一个字节为:11 001 011,即16进制CBH
由于这条语句没有内存寻址,SIB列为空,也没有偏移量列Displayment,这条语句也没有立即数作为操作数,所以Immediate列为空
至于Prefix列,我们稍微看下Prefix的说明和他的值表就能知道,Prefix列只有少数的几种情况才能出现,比如段超越啊,16位/32位切换啊,锁定啊,像mov bx,cx这样普通的语句自然也没有Prefix列
所以我们可以得到mov bx,cx的最终代码为
|
Prefix
|
Opcode
|
ModR/M
oo|rrr|mmm |
SIB
ss|iii|bbb |
Displayment
|
Immeidate
|
mov bx,cx |
|
100010001 |
11 001 011 |
|
|
|
mov cx,bx |
|
|
|
|
|
|
mov cl,bl |
|
|
|
|
|
|
既然已经掌握了mov bx,cx,那么mov cx,bx呢? mov cl,bl呢?大家自己想想
如果觉得上面例子还是太简单了,毕竟6列只用了2列,那么我们就来挑战一个有点难度的怎么样
二) mov [ebx+esi+1],dword 00h
word是nasm的关键字,表明存入内存的操作数是一个双字,在内存中占32位,即4个字节
查询Opcode,得1100011w,w=1,即C7
现在来看ModR/M,这里会有些变化了,我们要仔细分析我们的内存寻址方式ebx+esi+1,有一个8位的偏移量1,所以oo=01,后面的rrr和 mmm该指明用于寻址的两个寄存器,ebp和esi,查询rrr表,应该分别是011,110,则rrr=011,mmm=110,但是我偏偏不这样作, 我设置rrr为000(EAX),mmm为100(ESP),于是代码变为了01000100,44h
奇怪?明明是ebx+esi,怎么偏偏让你给变成了eax+esp了?
其实在查询mmm的时候,我们不应该查询rrr表,应该查询iii表,iii表是专门查询变址寄存器号码的,rrr表和iii表基本上完全相同,只是 rrr表中100代表ESP,而iii表中呢.....no index....,这不是表示没有变址寄存器,而是表示设置两个寄存器的工作交给后面的SIB来做,44h可以看做是个特殊的数字,这个数字就表明寻址方式所用的寄存器会让SIB位来完成.
上面的做法不是我别出心裁,其实如果你用nasm编译这句话,也会得到这个结果,让SIB来设置内存寻址,我想至少有两个好处,
一是可以更加灵活一些,毕竟人家SIB有整整一个字节专门来作这件事情,比如如果寻址模式位改为ebx+esi*2+1,SIB里专门有两位ss,表示这个倍数,而ModR/M里呢,对不起,没地方放了
二是可以让汇编编译器简单一些:统一成一种格式方便处理
ok,那么如果我们严格按照寄存器查表的结果(ebx=011,esi=110)能不能运行呢,大家自己去试试吧
SIB
ss:没有倍数,ss=00
iii:刚才查过了esi=110
bbb:ebx=011
合起来是00110011即33
后面是8位的偏移量,01h,最后是立即数00h,注意这里是个双字,所以占4个字节
填在表里
|
Prefix
|
Opcode
|
ModR/M
oo|rrr|mmm |
SIB
ss|iii|bbb |
Displayment
|
Immeidate
|
mov [ebx+esi+1],dword 00h |
67,66 |
C7 |
44 |
33 |
01 |
0000 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
你可能用nasm汇编了一下这条语句,发现前面多了个67,66,恭喜你,67和66正是Prefix,由于你是在16位环境下汇编的,所以如果某条指令使用到32位的数据和地址,指令前面就会出现前缀,67表示使用了32位地址,66表示使用了32位数据.消除的方法是在文件头上加上[BITS 32]
推荐一个好的机器码入门<老罗的OPCODE教程:http://www.luocong.com/learningopcode.htm>,x86 OPCODE规范下载<>
让人迷惑的倒置 -LittleEndian
参见上面的代码,MOV到ax的操作数为16位二进制的一,即0001h(h表示16进制)可是从这里看上去,是0100h,这是为什么呢?
其实这是著名的Little Endian存储格式捣的鬼,Little Endian的意思是高位在高地址,低位在低地址,比如0100 0011 0010 0001这个二进制数(十六进制为4321h),在内存里类似
位置 |
00 |
01 |
02 |
03 |
04 |
05 |
06 |
07 |
08 |
09 |
10 |
11 |
12 |
13 |
14 |
15 |
值 |
1 |
0 |
0 |
0 |
0 |
1 |
0 |
0 |
1 |
1 |
0 |
0 |
0 |
0 |
1 |
0 |
显示的时候,显示程序一般都以一个字节为整体显示这个数,即先解析处0-7位,为数字21h,显示在前面,然后解析8-16位,为数据43h,显示在后面,则变为了21h 43h,如果显示程序能按照字为整体解析并显示,就能没有这个倒装了,但是显示是不会知道你到底需要怎么显示的,比如你可以定义一个32位数据,也可能定义64位数据,即使是按照16位,也仍然会有倒装发生,所以现在一般显示程序都简单按照字节显示
除了LittleEndian反过来当然也有BigEndian,这种存储格式就和咱平时的数字理解习惯没有冲突了
LittleEndian 是Intel x86(8086/8088,80286,80x86,PentiumX)系列CPU所采用的格式,而BigEndian是Motorola的 PowerPC系列CPU所采用的标准,网络传输也采用BigEndian,二者各有优缺点,有兴趣的读者可以参考1980年的著名论文<On Holy Wars and a Plea for Peace>
别看LittleEndian这个是个细节,却绊倒了不少初学者的腿,比如你刚打开Windbg,想尝试利用调试工具修改某个游戏角色的体力值,从 157110修改为100000000,157110的16进制为265B6,而你在内存里怎么都找不到02 65 B6这个序列,那就是LittleEndian搞的鬼
相关推荐
在本教程中,你将学习如何编写、调试和优化汇编语言程序,这对于理解计算机底层工作原理、提高程序性能以及解决特定硬件问题具有重要意义。 汇编语言的基础知识包括以下几个方面: 1. **指令集架构**:IBM-PC采用...
这份“android 底层系统开发资料”涵盖了这一领域的核心分析和平台介绍,对于深入理解Android系统的运行机制至关重要。下面将详细探讨这些知识点。 一、Android系统结构 Android系统是一个基于Linux内核的开源操作...
总之,“TI LM3S系列底层程序”涵盖了嵌入式系统开发的关键环节,从硬件接口的驱动编写到系统级的程序设计,是学习和掌握LM3S5749及其相关型号的重要参考资料。通过深入研究这些内容,开发者可以提升其在嵌入式领域...
在智能手机领域,系统优化是一个持续而深入的课题,随着硬件性能的提升和软件需求的增加,对于智能手机底层系统优化的需求也在不断增长。在本次演讲中,吴章金先生主要介绍了从M9到PRO5智能手机的底层系统优化的演进...
这个教程旨在帮助开发者深入理解Android系统的底层工作原理,从而更好地进行应用开发和系统优化。 Android系统由以下几个层次构成: 1. **Application(应用程序层)**:这是用户直接接触的部分,包含Android内置...
而“清华大学计算机教程之-数据结构”可能是包含课程内容的PDF文档,涵盖理论讲解、实例演示和习题解答,通过逐步学习和实践,学生可以系统地掌握数据结构的原理和应用。 总的来说,数据结构和程序设计是构建高效...
标题中的“46-001-T”可能是一种型号或者版本编号,“烽火hg680-ka-mv300处理器”...同时,这也是一份学习和实践Android系统刷机技术的资源,对于想要深入理解Android系统和设备底层操作的IT爱好者来说具有很高的价值。
《算王安装算量ET199写锁工具——深入解析与应用指南》 在工程领域,特别是建筑行业的工程量计算中,精确、高效的算量软件是不可或缺的工具。"算王安装算量"就是这样一款专业软件,它以其强大的功能和易用性深受...
这个开发包包含了必要的驱动程序、API接口、示例程序以及详细的文档资料,以确保用户能够顺利地理解并使用这两款读写器。 1. **读卡器介绍**: - eye-U010和M1-U010是明华澳汉公司推出的智能卡读写设备,它们支持...
在本文中,我们将深入探讨Xilinx公司的xdma驱动下的底层读写DLL封装技术,这是针对PCI Express(PCIE)开发中的一个关键环节。Xilinx的xdma IP核是用于实现高性能PCIE接口的一种解决方案,而将底层读写操作封装成DLL...
《IBM-PC汇编程序设计教程》是一本深入解析IBM-PC平台汇编语言编程的教程,适合初学者和有经验的程序员进一步提升技能。该教程以PPT形式呈现,内容涵盖8个主要章节,旨在全面讲解汇编语言的基础概念、指令系统、程序...
《汇编程序设计教程》是针对计算机编程领域中基础但至关重要的汇编语言进行深入讲解的教程。汇编语言是一种低级编程语言,它与计算机硬件的指令集紧密相关,是程序员直接控制计算机硬件的手段之一。本教程适用于对...
总之,这份"Linux设备驱动程序学习教程"是学习Linux系统底层编程和硬件交互的理想资料,通过170页的详尽内容,可以系统性地掌握设备驱动开发的各个方面,为成为一名合格的Linux系统开发者奠定坚实基础。
这种方式大大降低了分布式系统中的通信复杂性,使得开发者可以更专注于业务逻辑,而无需关心底层网络通信细节。Dubbo作为一个优秀的RPC框架,它提供了服务注册、服务发现、负载均衡、容错机制等核心功能。 在Dubbo...
《深入理解计算机系统》第三章主要探讨的是程序的机器级表示,这是一门涉及底层计算机运作原理的关键领域。本章内容被分为五个部分,分别涵盖了基础、控制、数据、过程和高级主题,这些主题旨在帮助读者理解计算机...
总之,"cpp-两千行的教程代码教你如何构建自己的深度学习系统"是一次宝贵的学习机会,它将带你深入了解深度学习的底层工作原理,以及如何用C++实现一个高效的深度学习框架。通过这个项目,你不仅可以提升编程技能,...
《G广联达-ET199写锁163数据底层加驱动》是一个针对建筑行业信息化管理的专业技术教程,其核心内容涉及到了广联达软件的ET199写锁技术和163数据处理,以及相关的底层驱动程序开发。在深入理解这个主题之前,我们需要...
《C语言程序设计--期刊管理系统》是一份详细的教学资料,主要针对使用C语言进行程序设计,特别是构建一个期刊管理系统的实践教程。C语言是计算机科学中的基础编程语言,以其高效、灵活和对底层硬件的控制力强而著称...