python写系统底层_深入系统底层--之--教你用0101写程序

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]

推荐一个好的机器码入门,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年的著名论文

别看LittleEndian这个是个细节,却绊倒了不少初学者的腿,比如你刚打开Windbg,想尝试利用调试工具修改某个游戏角色的体力值,从157110修改为100000000,157110的16进制为265B6,而你在内存里怎么都找不到02 65 B6这个序列,那就是LittleEndian搞的鬼

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值