问题描述
|
出于兴趣,我想用机器代码编写一个小程序。
我目前正在学习有关寄存器,ALU,总线和内存的信息,我对指令可以用二进制而不是汇编语言编写感到有些着迷。
是否需要使用编译器?
最好在OSX上运行。
解决方法
您不会使用编译器来编写原始机器代码。您将使用十六进制编辑器。不幸的是,我没有使用OSX,因此无法为您提供指向该链接的特定链接。
如果编写机器代码,则还需要学习如何编写操作系统所需的二进制标头。我建议这样做,并首先使用原始输出格式的汇编程序进行测试;一旦您了解了二进制布局,将其手工组装为机器代码就是纯粹的机械任务。
,您将使用十六进制编辑器。我建议不要这样做,而是先学习汇编器。汇编程序基本上是一种在人类可读的助记符和机器可读的十六进制字节之间具有1:1对应关系的语言。为此,您可能希望查看http://ref.x86asm.net/并找到可在x86 Mac上运行的汇编程序。我相信Yasm应该有用。
用十六进制直接编写任何东西非常困难,您可能需要花费时间来学习汇编和汇编程序生成的基础机器代码。
,您确实需要一个汇编程序,就像其他张贴者所说的那样,编写二进制指令代码是如此的令人麻木,而且必须如此正确,以至于只有一台机器才能做到。在非平凡的OS(例如OSX)上。在Linux,Windows中,必须提供正确的标头信息才能生成可执行文件。同样,最好通过汇编程序包来完成此操作,该程序包可以链接正确的标头,以确保您具有指令的数据,堆栈和执行。然后,您的汇编程序将一次又一次崩溃,直到:D。
编写二进制指令通常被归类为酷刑。这样做侵犯了基本人权。如果有人要求您将其外包给Gitmo。
获取一个汇编器。
Rgds,
马丁
,编译器将您的非机器代码变成机器代码...因此您不需要编译器...
,如果要将机器代码包含在具有元数据的标准目标文件中,以便可以将其链接并从C程序调用它,则可能仍要使用汇编程序。
除了目标文件元数据之外,这还为您提供了能够编写注释的巨大优势。还有标签,使汇编器可以计算出位移,以进行手动跳转编码,例如
db 0xE8
; dd target - ($ + 4)
编码x86jmp 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确实有一个方便的平面二进制输出模式,您可以使用该模式仅获取机器代码字节,而无需包含目标文件,而无需将ѭ9转换为平面二进制或ѭ10。
您可以像这样在x86-64 MacOS的asm中编写“ 11”。 (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基础吗?以及如何阅读Intel Opcode表示法提供了有关指令编码的一些详细信息。理解这些工作原理足以使您对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
将其与相同的“ 19”组合在一起,将形成一个字节一个字节的相同目标文件。 cmp mac-*.o
不输出任何输出并返回true。您可以使用with21ѭ将其链接到C程序。
机器代码的愚蠢计算机技巧
您可以在机器代码中执行的有趣事情之一,而不是汇编语言,则是一条指令与其他指令重叠,例如输入4个字节的循环,其中1个字节的操作码为cmp eax,imm32
,而不是2个字节的jmp rel8
。但这仅对“代码高尔夫”有用(优化代码大小会牺牲其他所有方面,包括性能)。
当现代CPU必须从不同于其开始解码的起始点开始解码某些代码字节时,它们不喜欢它。一些AMD CPU在L1i缓存中标记指令边界。我忘记/为什么英特尔CPU会有问题。我不确定在uop缓存中是否会发生冲突; Agner Fog的微体系结构指南针对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
双字。通常,您会在循环中将“ѭ26”添加到标签上,或者从第一次迭代中剥离“ load + test”以有条件地跳过循环,或者落入循环中以存储第一个元素(如果它应非零次运行)。 (为什么循环总是编译成“ 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)
取第一个“ 28”之后,它解码为:
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,eax
e: 75 f3 jne 3 <_copy_nonzero_ints.loop>
10: c3 ret
也可用于db 0xb9,0x7b
之类的内容:mov ecx,123
的前2个字节,它将接下来的3个字节用作立即数的高字节。将CL留给已知值,ECX的高字节取决于代码的3个字节。如果可以找到具有所需编码的指令,则实际上可以将代码用作有用的立即数据。
上面的循环只是一个虚构的示例,用来说明该技巧的可能用例。这不是实现该功能的最有效方法;如果实际打高尔夫球的代码大小,您可能会使用lodsd
和stosd
。
而且,与使用SSE2一次复制+检查4个双字相比,这是相当慢的,因此通常不会为提高性能而编写此字。但是,假设您正在针对代码大小进行优化。 (并参阅x86 / x64机器代码中的打高尔夫球技巧)
另外,您可能会相对于dst索引src,例如在循环之前为sub rsi,rdi
,因此您可以在循环内部使用ѭ35with以及mov [rdi-4],eax
个存储(可以在Intel的端口7上运行,因此对超线程更友好)和mov eax,[rsi+rdi]
负载。