如何在不使用汇编的情况下为 x86 编写原始机器代码?

How can I write raw machine code for x86 without using assembly?

我希望能够编写原始机器代码,无需汇编或任何其他类型的高级语言,可以直接放入闪存驱动器和 运行。我已经知道要使其正常工作,我需要将主引导记录 headers(我已设法手动完成)格式化到驱动器上。我已经完成了这个,并且成功地能够在我的代码所在的驱动器的第一个扇区(在本例中为前 512 个字节)中使用汇编代码在屏幕上显示一行文本。但是,我希望能够将原始十六进制代码写入驱动器,就像我对 MBR 格式化所做的那样,而无需任何类似汇编的工具来帮助我。我知道有一种方法可以做到这一点,但我还没有真正找到任何没有提到组装的东西。我在哪里可以找到这方面的信息?谷歌搜索机器代码或 x86 编程得出汇编,这不是我想要的。

http://ref.x86asm.net/coder32.html

虽然我真的不明白,但你为什么would.do这个。

如果您真正想要的是更好地理解 x86 机器代码,我建议您先查看汇编程序的输出,看看它为每一行汇编了哪些字节到输出文件中汇编源代码。

nasm -fbin -l listing.txt foo.asm 将为您提供一个包含原始十六进制字节和源代码行的列表,或者 nasm -fbin -l/dev/stdout foo.asm | less 将列表直接传送到文本查看器中。有关输出的示例,请参阅 codegolf.SE 上的 this chroma-key blend function in 13 bytes of x86 machine code I wrote

您也可以在正常创建二进制文件后对其进行反汇编。 ndisasm 适用于平面二进制文件,并生成相同格式的十六进制字节 + asm 指令。 objdump 等其他反汇编程序也可用:Disassembling A Flat Binary File Using objdump.

半相关:


Intel 的 x86 手册完全指定了指令的编码方式:请参阅 the vol.2 insn set reference manual,第 2 章指令格式以了解前缀、操作码、ModR/M +可选的 SIB 和可选的位移,以及即时的。

鉴于此,您可以阅读有关如何对其进行编码的每条指令文档,例如 D1 /4 (shl r/m32, 1) 表示操作码字节为 D1,而 ModRM 的 /r 字段必须为 4。 (对于某些指令,/r 字段用作 3 个额外的操作码位。)

还有一个附录将操作码字节映射回指令,以及该手册中的其他部分。

可以当然可以使用十六进制编辑器输入您手动计算的编码,从而在不使用汇编程序的情况下创建 512 字节的二进制文件。但这是一个毫无意义的练习。


有关 x86 指令编码的许多怪癖,另请参阅 tips for golfing in x86 machine code:例如inc/dec 一个完整的寄存器有单字节编码(64 位模式除外)。它当然专注于指令 length,但除非您坚持自己查找实际编码,否则有趣的部分是哪些形式的指令具有不同或特殊的可用编码。 objdump -d 显示机器代码字节和 AT&T 语法反汇编的输出中有几个关于该技巧 Q&A 的答案。

只是为了画图...

首先,您不会找到如何用机器代码编程,它没有与之关联的程序集,这应该是显而易见的。你会发现任何体面的指令参考都包含一些 assembler 的汇编以及机器代码,因为你确实需要某种方式来引用一些位模式,而汇编语言就是这种语言。

因此查找 nop,例如您会发现位模式 10010000 或 0x90。因此,如果我想将指令 nop 添加到我的程序中,我将添加字节 0x90。因此,即使你回到非常早期的处理器,你仍然希望用汇编语言编程并用铅笔和纸手 assemble 然后使用 dip 开关将程序计时到内存中,然后再尝试 运行 它。因为这是有道理的。几十年后,甚至为了演示机器代码编程,尤其是像 x86 这样痛苦的指令集,你从汇编开始,assemble,然后是 dissassemble,然后再谈论它,所以这里是:

top:
    mov ah,01h
    jmp one
    nop
    nop
one:
    add ah,01h
    jmp two
two:
    mov bx,1234h
    nop
    jmp three
    jmp three
    jmp three
three:
    nop
    jmp top

nasm -f aout so.s -o so.elf
objdump -D so.elf

00000000 <top>:
   0:   b4 01                   mov    [=10=]x1,%ah
   2:   eb 02                   jmp    6 <one>
   4:   90                      nop
   5:   90                      nop

00000006 <one>:
   6:   80 c4 01                add    [=10=]x1,%ah
   9:   eb 00                   jmp    b <two>

0000000b <two>:
   b:   66 bb 34 12             mov    [=10=]x1234,%bx
   f:   90                      nop
  10:   eb 04                   jmp    16 <three>
  12:   eb 02                   jmp    16 <three>
  14:   eb 00                   jmp    16 <three>

00000016 <three>:
  16:   90                      nop
  17:   eb e7                   jmp    0 <top>

所以只有前几条指令描述了问题以及为什么 asm 如此有意义...

第一个你可以很容易地用机器代码编程 b4 01 mov ah,01h 我们进入文档中的重载指令 mov 并找到要注册的立即操作数。 1011wreg 数据我们有一个字节所以它不是一个字所以字位没有设置,我们必须查找 reg 以找到 ah 以 b4 结尾并且立即数是 01h。还不错,但现在跳 我想跳过一些东西,那么多少东西?我想使用哪个跳跃?我要保守一点,用最少的字节吗?

我可以看出我想跳过两条指令我们可以很容易地查一下nop就知道它们是一个字节,0x90,指令。所以段内直接做空应该像 assembler 选择的那样工作。 0xEB 但偏移量是多少? 0x02 跳过我现在和我想去的地方之间的两个 BYTES 指令。

因此,您可以阅读我从英特尔文档中 assembled 获得的其余说明,以了解 assembler 选择这些字节的内容和原因。

现在正在看intel 8086/8088手册,段内直接短指令注释了sign extended,段内direct没有说sign extended,虽然此时的处理器是16位,但你有更多的段,所以只阅读手册,无法访问设计工程师,并且没有使用调试的 assembler 作为参考,我怎么知道我是否可以使用 16 位直接跳转到向后分支的最后一条指令?在这种情况下,assembler 选择了字节大小的偏移量,但是如果...

我使用的是 16 位手册,但使用的是 32/64 位工具,所以我必须考虑到这一点,但我可以而且确实这样做了:

three:
    nop
db 0xe9,0xe7,0xff,0xff,0xff

而不是 jmp 顶部。

00000016 <three>:
  16:   90                      nop
  17:   e9 e7 ff ff ff          jmp    3 <top+0x3>

对于 8086 应该是 0xe9,0xe7,0xff

   db 0xb4,0x01
   db 0xeb,0x02
   db 0x90
   db 0x90

所以现在如果我想将其中一个被跳过的 nop 更改为 mov

   db 0xb4,0x01
   db 0xeb,0x02
   db 0xb4,0x11
   db 0x90

但现在它坏了我必须修复跳跃

   db 0xb4,0x01
   db 0xeb,0x03
   db 0xb4,0x11
   db 0x90

现在将其更改为添加

   db 0xb4,0x01
   db 0xeb,0x03
   db 0x80,0xc4,0x01
   db 0x90

现在又要改跳转了

   db 0xb4,0x01
   db 0xeb,0x04
   db 0x80,0xc4,0x01
   db 0x90

但是如果我用汇编语言编写了那个 jmp 程序,我就不必处理 assembler 做的事情。当你的跳跃恰好在那个距离的尖端时,情况会变得更糟,然后你说在那个循环中还有一些其他的跳跃,你必须多次检查代码以查看是否有任何其他跳跃是 2 或 3 或 4 字节,这是否会推动我较长的跳转从一个字节到另一个字节

a:
...
jmp x
...
jmp a
...
x:

当我们通过跳转 x 时,我们是否为它分配了 2 个字节?然后到 jmp a,也为它分配两个字节,到那时我们可能已经弄清楚了所有其余的 jmp a 和 a: 之间的指令,它正好适合两个字节的跳转。但最终我们到达 x: 发现 jmp x 需要 3 个字节,这将 jmp a 推得太远了,现在它必须是一个三字节的 jmp,这意味着我们必须回到 jmp x 并调整来自 jmp a 的附加字节现在是三个字节,而不是假设的 2 个字节。

assembler 会为您完成所有这些工作,如果您想首先直接编写机器代码,并且最重要的是,您将如何在没有一些自然语言注释的情况下跟踪数百条不同的指令轨道?

所以我可以做到这一点

    mov ah,01h
top:
    add ah,01h
    nop
    nop
    jmp top

然后

nasm so.s -o so
hexdump -C so
00000000  b4 01 80 c4 01 90 90 eb  f9                       
|.........|
00000009

或者我可以这样做:

#include <stdio.h>
unsigned char data[]={0xb4,0x01,0x80,0xc4,0x01,0x90,0x90,0xeb,0xf9};
int main ( void )
{
    FILE *fp;
    fp=fopen("out.bin","wb");
    if(fp==NULL) return(1);
    fwrite(data,1,sizeof(data),fp);
    fclose(fp);
}

我想在循环中添加一个 nop:

    mov ah,01h
top:
    add ah,01h
    nop
    nop
    nop
    jmp top

#include <stdio.h>
unsigned char data[]={0xb4,0x01,0x80,0xc4,0x01,0x90,0x90,0x90,0xeb,0xf8};
int main ( void )
{
    FILE *fp;
    fp=fopen("out.bin","wb");
    if(fp==NULL) return(1);
    fwrite(data,1,sizeof(data),fp);
    fclose(fp);
}

如果我真的想用机器代码编写,我将不得不这样做:

unsigned char data[]={
0xb4,0x01, //top:
0x80,0xc4,0x01, //add ah,01h
0x90, //nop
0x90, //nop
0x90, //nop
0xeb,0xf8 //jmp top
};

保持理智。有一些指令集是我为了好玩而为自己制作的,并且更容易用机器代码编程,但仍然更好地使用汇编助记符在伪代码中注释...

如果您的目标只是简单地以某种格式、裸机或其他非 Windows 或 Linux 文件格式程序的一些机器代码块结束,您可以使用汇编语言并在从汇编源代码到二进制机器代码结果的工具链的一两个步骤。最坏的情况是您编写一个临时程序以从工具链的输出中获取,并将这些位操作为其他位。最后,您无需丢弃可用于手动写入原始位的工具,只需重新格式化输出文件格式即可。

在 Python 中,您可以使用子进程模块和 hexdump.py 由 anatoly techtonik techtonik@gmail.com 创建的 Public 域程序,它最适合采用任何已编译的语言类型并以全文形式获取原始机器代码和 asm。

其次是 Pelles C。版本 9.0 C11-17 在 Pelles 中,您只需在调试一次后再次调试。它会为您吐出机器代码和汇编代码。很好,但是您不能复制和粘贴代码。您可以看到所有内容,但如果需要,则必须手动输入。

两者都用于开发新的编程语言。主要是因为您可以在构建词法分析器并通过它设置机器指令时看到指令死机。

我对编写原始机器的看法是这样的-->如果你犯了一个错误,你就会失去任何类型的致命错误检测或有条件的 try catch 来调试或检查它,然后再通过并损坏你机器中的东西。

这正是我们拥有计算机语言的原因。在开始编写原始代码之前,最好使用 C 或 C++ 内联 ASM 方法进行测试。您将需要在此处找到的 x86 指令集。

x86 Instruction Sets 无论如何都要保证安全。