只是出于兴趣,我想用机器代码编写一个小程序。
我目前正在学习寄存器、ALU、总线和内存,我对指令可以用二进制而不是汇编语言编写有点着迷。
是否需要使用编译器?
最好是在 OSX 上运行的。
只是出于兴趣,我想用机器代码编写一个小程序。
我目前正在学习寄存器、ALU、总线和内存,我对指令可以用二进制而不是汇编语言编写有点着迷。
是否需要使用编译器?
最好是在 OSX 上运行的。
您不会使用编译器来编写原始机器代码。您将使用十六进制编辑器。不幸的是,我不使用 OSX,所以我无法为您提供具体的链接。
如果您编写机器代码,您还需要学习如何编写操作系统所需的二进制标头。我建议这样做并首先使用原始输出格式的汇编程序进行测试;一旦您了解了二进制布局,将其手动组装成机器代码就是一项纯粹的机械任务。
您将使用十六进制编辑器。我建议不要这样做,而是先学习汇编程序。汇编程序基本上是一种在人类可读的助记符和机器可读的十六进制字节之间具有 1:1 对应关系的语言。为此,您可能希望查看http://ref.x86asm.net/并找到适用于 x86 Mac 的汇编程序。我相信yasm应该工作。
直接用十六进制编写任何东西都非常困难,您可能会花时间学习汇编和汇编器生成的底层机器代码
你需要一个汇编器,你真的需要,正如其他发帖者所说,编写二进制指令代码是如此无聊,而且必须如此正确,以至于只有机器才能做到。在非平凡的操作系统上,比如 OSX。Linux、Windows,必须提供正确的头信息才能生成可执行文件。同样,这最好通过一个汇编程序包来完成,该程序包可以链接正确的头文件,以确保您有数据、堆栈和执行指令。然后,您的汇编程序将崩溃,一次又一次,很长时间:D。
编写二进制指令通常被归类为折磨。这样做违反了基本人权。如果您被要求这样做,请将其外包给 Gitmo。
得到一个汇编程序。
Rgds,马丁
编译器将您的非机器代码转换为机器代码......所以你不需要编译器......
如果您希望您的机器代码包含在标准目标文件中,并带有元数据,以便您可以将其链接并从 C 程序中调用它,您可能仍希望使用汇编程序。
除了对象文件元数据之外,这还为您提供了能够编写注释的巨大优势。还有标签让汇编器计算手动跳转编码的位移,例如db 0xE8
;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, r32
之间。add r32, r/m32
由于您使用的是 MacOS,因此 NASM 不是最可靠的选择。它的 MachO64 输出格式支持存在多个错误。AFAIK 当前版本有效,但您可能更愿意使用 GNU 汇编器(OS X 的默认编译器 clang 可以汇编)。
OTOH,NASM 确实有一个方便的平面二进制输出模式,您可以使用它来获取机器代码字节,而无需使用它们周围的目标文件,而不必使用objcopy
平面二进制或ld
.
您可以像这样为 x86-64 MacOS 编写int add(int a, int b) { return a+b; }
asm。(MacOS 在 C 名称前加上前导下划线)
;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 + 可选额外字节,然后是可选立即数,您会发现查找实际操作码编号通常无趣;有趣的是通常只是指令长度。 或者任何你感兴趣的东西,你可以在反汇编输出中查看。)
例如,不允许将 rbp 作为 SIB 基础?和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
.
您可以在机器代码中而不是 asm 中做的一件有趣的事情是让指令与其他指令重叠,例如使用 1 字节操作码cmp eax, imm32
而不是 2 字节输入循环 4 字节jmp rel8
。但这仅对“代码高尔夫”有用(以牺牲其他一切为代价优化代码大小,包括性能)。
现代 CPU 不喜欢它必须从与已经解码的起点不同的起点解码一些代码字节。一些 AMD CPU 在 L1i 缓存中标记指令边界。我忘记了英特尔 CPU 是否/为什么会出现问题。我不确定它是否会在 uop 缓存中发生冲突;Agner Fog 的微架构指南对 Sandybridge 说“如果同一段代码有多个跳转条目,则它可以在 μop 缓存中有多个条目。 ”,但 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
请注意,我们在底部有我们想要的循环分支,但是我们在存储之前加载并测试了一个 dword。这个假设的循环不想存储终止0
dword。通常你会jmp
进入一个标签的循环,或者从第一次迭代中剥离负载+测试以有条件地跳过循环,或者如果它应该运行非零次,则进入循环以存储第一个元素。(为什么循环总是编译成“do...while”风格(尾跳)?)
第一次通过循环,它解码为
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
:前 2 个字节mov ecx, 123
消耗接下来的 3 个字节作为立即数的高字节。给 CL 留下一个已知值,ECX 的高字节依赖于这 3 个字节的代码。如果您可以找到具有所需编码的指令,您实际上可以将您的代码用作有用的即时数据。
上面的循环只是一个虚构的示例,用于说明该技巧的可能用例。这不是实现该功能的最有效方式;你可能会使用lodsd
,stosd
如果真的打高尔夫球来获得代码大小。
此外,与使用 SSE2 一次复制 + 检查 4 个 dwords 相比,这非常慢,因此您通常不会为了性能而编写此代码。 但是假设您正在优化代码大小。(请参阅使用 x86/x64 机器码打高尔夫球的技巧)
此外,您可以相对于 dst 索引 src,就像sub rsi, rdi
在循环之前一样,因此您可以add rdi, 4
在循环内部使用mov [rdi-4], eax
存储(可以在 Intel 的端口 7 上运行,因此这对超线程更友好)和mov eax, [rsi+rdi]
加载。