.data
.global _start
_start:
mov r7, #4
mov r0, #1
mov r2, #12
ldr r4, =#0x6c6c6548
str r4, [pc, #4]
mov r1, pc
add pc, pc, #8
strbt r6, [ip], -r8, asr #10
svcvs 0x0057206f
beq 0x193b248
swi #0
mov r7, #1
mov r0, #0
swi #0
我偶然发现了这个小的 ARM 汇编程序,它打印“Hello World”。将其保存为 test.s
进行测试:
$ as -o test.o test.s
$ ld -o test test.o
$ ./test
Hello World
$
这是如何工作的?我在整个程序中看不到任何一个字符串。它也不会从其他地方读取字符串;看起来这段代码就是打印字符串所需的全部代码。字符串从哪里来?
最佳答案
这是一个有趣的注释:
mov r7, #4
mov r0, #1
mov r2, #12
ldr r4, =#0x6c6c6548
A str r4, [pc, #4]
B mov r1, pc
C add pc, pc, #8
D strbt r6, [ip], -r8, asr #10
E svcvs 0x0057206f
F beq 0x193b248
G swi #0
mov r7, #1
mov r0, #0
swi #0
A
处的存储目标位置 D
- 正如评论中指出的,该单词(按小端顺序)创建 4 个 ASCII 字节“Hell” -它存储在无意义指令的顶部(其机器代码是 0xe66c6548 - 接近,但不够好)。这大概也是为什么它位于数据部分的原因,以确保它是可写的*。同时,E
处指令的机器码为0x6f57206f,即“o Wo”。指令 F
特别棘手,因为该地址必须产生相对分支偏移量,一旦编码,看起来像“rld”** - beq
编码为 0x0annnnnn,其中 nnnnnn 是 26 位二进制补码偏移值的前 24 位 - 另请注意,顶部字节中的条件代码和操作码构成了最终的换行符。
指令B
将D
的地址放入r1,即指向字符串开头的指针。 r0 和 r2 显然是其他必要的系统调用参数,而 r7 是系统调用号本身(我懒得查找它,但我假设 r0 中的 1 用于 stdout,r2 中的 12 是字符数,系统调用 4 是write
)。
最后,指令C
是跳转到G
处的系统调用,因此D
处没有任何“指令”, E
和 F
实际上被执行(之后的其余部分只是进行 exit
系统调用)。
非常简洁,适合技巧代码。
* 并且可能还依赖加载程序中的某些向后兼容行为来使数据部分可执行。
** 这在我的 binutils 2.26 链接器中不会发生,可能是由于最近版本中默认的节对齐方式发生了变化。
关于assembly - 这个汇编程序如何打印 "Hello World"?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38294896/