我会用什么编译器来编写机器语言?

时间:2011-05-25 17:53:57

标签: assembly machine-code

出于兴趣,我想在机器代码中编写一个小程序。

我目前正在学习寄存器,ALU,总线和内存,我有点着迷,指令可以用二进制而不是汇编语言编写。

是否需要使用编译器?

最好在OSX上运行。

5 个答案:

答案 0 :(得分:5)

您不会使用编译器来编写原始机器代码。您将使用十六进制编辑器。不幸的是,我不使用OSX,因此我无法为您提供一个特定的链接。

如果您编写机器代码,您还需要学习如何编写操作系统所需的二进制头文件。我建议首先使用原始输出格式的汇编程序进行测试;一旦你理解了二进制布局,将它手工组装成机器代码就是一个纯粹的机械任务。

答案 1 :(得分:3)

您将使用十六进制编辑器。我建议不要这样做,先学习汇编程序。汇编程序基本上是一种语言,在人类可读助记符和机器可读的十六进制字节之间具有1:1的对应关系。为此,您可能希望查看http://ref.x86asm.net/并找到适用于x86 Mac的汇编程序。我相信yasm应该有效。

直接用十六进制编写任何东西都非常困难,你的时间可能花在学习汇编和汇编程序生成的底层机器代码上

答案 2 :(得分:1)

你需要一个汇编程序,你真的这样做,正如其他海报所说的那样编写二进制指令代码是如此令人烦恼无聊,并且必须如此正确,只有机器应该这样做。在非平凡的操作系统上,如OSX。 Linux,Windows,必须提供正确的头信息才能生成可执行文件。同样,最好由汇编程序包完成,该程序包可以链接正确的标头,以确保您有指令的数据,堆栈和执行。然后,你的汇编程序会崩溃,并且会再次发生故障:D。

写二进制指令通常被归类为酷刑。这样做违反了基本人权。如果您被要求这样做,请将其外包给Gitmo。

获取汇编程序。

RGDS, 马丁

答案 3 :(得分:0)

编译器将您的非机器代码转换为机器代码......因此您不需要编译器......

答案 4 :(得分:0)

如果您希望将机器代码包含在具有元数据的标准目标文件中,以便可以将其链接并从C程序调用它,则您可能仍希望使用汇编程序。

除目标文件元数据外,它还具有编写注释的巨大优势。还有标签,使汇编器像db 0xE8一样为manual jump encoding计算位移; dd target - ($ + 4)来编码x86 jmp rel32。或针对RIP相对寻址模式。


汇编器源代码通常使用add eax, ecx之类的助记符将字节01 c8汇编到输出文件(x86)中。但是该源代码行完全等同于NASM语法db 0x01, 0xc8(假设BITS 32或BITS 64)或GAS语法.byte 0x01, 0xc8

无论哪种方式,这些源代码行都将使汇编器将相同的2个字节输出到输出文件的当前节中。汇编程序就是这样做的:根据某些文本源将字节写入输出文件。 asm源代码是一种便捷的语言,可以直接与机器代码直接映射。对于x86,汇编器有几种选择,可以选择最短的编码,还可以选择两种可能的操作码之一,例如两个操作数都是寄存器时,add r/m32, r32add r32, r/m32之间。

由于您使用的是MacOS,所以NASM并不是最可靠的选择。它的MachO64输出格式支持中存在多个错误。 AFAIK当前版本可以运行,但是您可能宁愿使用GNU汇编程序(OS X的默认编译器clang可以进行汇编)。

OTOH,NASM确实有一个方便的平面二进制输出模式,您可以使用它来获取 just 机器码字节,而无需包含目标文件,而不必使用objcopy平面二进制或ld

您可以像这样在x86-64 MacOS的asm中编写int add(int a, int b) { return a+b; }。 (MacOS prepends C names with a leading underscore

;section .text        ; already the default if you haven't use section .data or anything

; NASM syntax:
global _add                    ; externally visible symbol name for linking
_add:
    lea   eax, [rdi+rsi]
    ret

我们可以将其与nasm -fmacho64 mac-add.asm组合在一起,并获得一个238字节的mac-add.o输出文件。通过使用db伪指令/伪指令写入字节,可以得到逐字节的相同输出文件。但是首先,让我们作弊并找出字节数,这样我们就不会浪费时间手动查看编码表。

(一旦您了解了如何将x86机器码指令,前缀,操作码,ModRM +可选的额外字节,然后是可选的立即数组合在一起的基础知识,通常会发现查找实际的操作码编号是没有意思的;有趣的事情通常只是指令 length 或者您有什么好奇的事,可以在反汇编输出中查看。

例如,rbp not allowed as SIB base?How to read the Intel Opcode notation提供了有关指令编码的一些详细信息。理解这些工作原理足以充分了解x86机器代码,而无需实际知道很多指令的具体编号。

$ objdump -d -Mintel mac-add.o
  (doesn't support MachO64 object files on my Linux desktop)
$ llvm-objdump -d -x86-asm-syntax=intel mac-add.o

mac-add.o:      file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
_add:
       0:       8d 04 37        lea     eax, [rdi + rsi]
       3:       c3      ret

因此在NASM源代码中,mac-raw-add.asm

global _add
_add:                     ; we're still letting the assembler make object-file metadata
  db 0x8d, 0x04, 0x37     ; lea eax, [rdi+rsi]
  db 0xc3                 ; ret

将其与相同的nasm -fmacho64组合在一起将构成一个逐字节的相同目标文件。 cmp mac-*.o不输出任何输出并返回true。您可以使用clang -O2 -g main.c mac-raw-add.o将其与C程序链接。


带有计算机代码的愚蠢计算机技巧

您可以在机器代码中做的有趣的事情之一,但不是asm,就是使一条指令与其他指令重叠,例如使用cmp eax, imm32的1字节操作码而不是2字节jmp rel8输入4字节的循环。但这仅对“代码高尔夫”有用(对代码大小进行优化会以其他所有代价为代价,包括性能在内)。

当现代CPU必须从不同于其开始解码的起始点解码某些代码字节时,它们不喜欢它。一些AMD CPU在L1i缓存中标记指令边界。我忘记/为什么英特尔CPU会有问题。我不确定在uop缓存中是否会发生冲突; Agner Fog's microarch guide says for Sandybridge如果有多个跳转条目,同一段代码在μop缓存中可以有多个条目。”,但如果IDK可以用于对相同字节的不同解码,则IDK。 >

无论如何,您可以做一些疯狂的事情,例如:

global _copy_nonzero_ints
_copy_nonzero_ints:      ;; void f(int *dst, int *src)

   xor  edx, edx
   db 0x3d       ; opcode for cmp eax, imm32.  Consumes the next 4 bytes as its immediate
   ;;   BAD FOR PERFORMANCE, DON'T DO THIS NORMALLY
.loop:                        ; do {
    mov  [rdi + rdx*4 - 4], eax    ; 4 bytes long: opcode + ModRM + SIB + disp8.  Skipped on first loop iteration: decoded as the immediate for cmp
    mov  eax, [rsi + rdx*4]
    inc  edx                       ; only works for array sizes < 4 * 4GB
    test eax, eax
    jnz  .loop                ; }while(src[i] != 0)

    ret

请注意,我们在底部具有所需的循环分支,但在存储双字之前先对其进行加载和测试。该假设循环不想存储终止的0双字。通常,您会jmp进入循环中的标签,或者从第一次迭代中剥离load + test以有条件地跳过循环,或者落入循环中以存储第一个元素(如果它应运行为非零值)次。 (Why are loops always compiled into "do...while" style (tail jump)?

第一次通过循环,它解码为

   0:   31 d2                   xor    edx,edx
   2:   3d 89 44 97 fc          cmp    eax,0xfc974489
   7:   8b 04 96                mov    eax,DWORD PTR [rsi+rdx*4]
   a:   ff c2                   inc    edx
   c:   85 c0                   test   eax,eax
   e:   75 f3                   jne    3 <_copy_nonzero_ints+0x3>

(from yasm -felf64 foo.asm  && objdump -drwC -Mintel foo.o
 YASM doesn't create visible symbol-table entries for .label local labels
 NASM does even if you don't specify extra debug info)

采用第一个jnz后,其解码为:

0000000000000000 <_copy_nonzero_ints>:
   0:   31 d2                   xor    edx,edx
   2:   3d                      .byte 0x3d

0000000000000003 <_copy_nonzero_ints.loop>:
   3:   89 44 97 fc             mov    DWORD PTR [rdi+rdx*4-0x4],eax
   7:   8b 04 96                mov    eax,DWORD PTR [rsi+rdx*4]
   a:   ff c2                   inc    edx
   c:   85 c0                   test   eax,eax
   e:   75 f3                   jne    3 <_copy_nonzero_ints.loop>
  10:   c3                      ret    

还可以处理db 0xb9, 0x7b之类的内容:mov ecx, 123的前2个字节消耗下3个立即数。将CL留给已知值,ECX的高字节取决于代码的3个字节。如果您可以找到具有所需编码的指令,则实际上可以将代码用作有用的立即数据。


上面的循环只是一个虚构的例子,用来说明该技巧的可能用例。这不是实现该功能的最有效方法。如果实际打高尔夫球以获得代码大小,则可能会使用lodsdstosd

此外,与使用SSE2一次复制+检查4个双字相比,这非常慢,因此,您通常不会为提高性能而编写此字。 但是假设您正在优化代码大小。 (并参见Tips for golfing in x86/x64 machine code

此外,您可以相对于dst索引src,如循环前的sub rsi, rdi,因此您可以在循环内使用add rdi, 4,并存储mov [rdi-4], eax(可以在端口7上的英特尔端口,因此更加友好(超线程)),并加载mov eax, [rsi+rdi]