我写了下面的代码,你能解释一下程序集在这里告诉我什么吗?
typedef struct
{
int abcd[5];
} hh;
void main()
{
printf("%d", ((hh*)0)+1);
}
组装:
.file "aa.c"
.section ".rodata"
.align 8
.LLC0:
.asciz "%d\n"
.section ".text"
.align 4
.global main
.type main, #function
.proc 020
main:
save %sp, -112, %sp
sethi %hi(.LLC0), %g1
or %g1, %lo(.LLC0), %o0
mov 20, %o1
call printf, 0
nop
return %i7+8
nop
.size main, .-main
.ident "GCC: (GNU) 4.2.1"
哦,哇,SPARC 汇编语言,我已经几年没见过了。
我想我们是一行一行地走? 我将跳过一些无趣的样板文件。
.section ".rodata"
.align 8
.LLC0:
.asciz "%d\n"
这是您在
printf
中使用的字符串常量(非常明显,我知道!)需要注意的重要事项是它位于 .rodata
部分(部分是最终可执行映像的划分;这部分用于“读取” -only data”并且实际上在运行时是不可变的)并且它被赋予了 label .LLC0
。 以点开头的标签是目标文件私有的。 稍后,当编译器想要加载字符串常量的地址时,它将引用该标签。
.section ".text"
.align 4
.global main
.type main, #function
.proc 020
main:
.text
是实际机器代码部分。 这是用于定义名为 main
的全局函数的样板标头,该函数在汇编级别与任何其他函数没有什么不同(在 C 中——在 C++ 中不一定如此)。 我不记得.proc 020
做了什么。
save %sp, -112, %sp
保存之前的寄存器窗口,向下调整堆栈指针。 如果你不知道什么是注册窗口,你需要阅读架构手册:http://sparc.org/wp-content/uploads/2014/01/v8.pdf.gz。 (V8 是 SPARC 的最后一个 32 位迭代,V9 是第一个 64 位迭代。这似乎是 32 位代码。)
sethi %hi(.LLC0), %g1
or %g1, %lo(.LLC0), %o0
这个两条指令序列的最终效果是将地址
.LLC0
(即字符串常量)加载到寄存器 %o0
,这是第一个 outgoing 参数寄存器。 (该函数的参数到位于incoming参数寄存器中。)
mov 20, %o1
将立即数 100 加载到第二个传出参数寄存器
%o1
中。 这是由 ((foo *)0)+1
计算得出的值。它是 20,因为您的 struct foo
是 20 字节长(五个 4 字节 int
),并且您要求数组中从地址 0 开始的第二个。
顺便说一下,只有当基指针的地址处实际上有一个足够大的数组时,计算指针的偏移量才在 C 中得到明确的定义;
((foo *)0)
是一个空指针,因此那里没有数组,因此表达式 ((foo *)0)+1
在技术上具有未定义的行为。 GCC 4.2.1,针对托管 SPARC,恰好将其解释为“假装在地址 0 处有一个任意大的 foo
数组,并计算数组成员 1 的预期偏移量”,但其他(尤其是较新的)编译器可能会做一些完全不同的事情。
call printf, 0
nop
致电
printf
。 我不记得零是做什么用的。 call
指令有一个延迟槽(再次阅读架构手册),其中填充了一条不执行任何操作的指令,nop
。
return %i7+8
nop
跳转到寄存器
%i7
中的地址加8。 这具有从当前函数返回的效果。
return
还有一个延迟槽,用另一个nop
填充。 该延迟槽中应该有一条 restore
指令,与函数顶部的 save
相匹配,以便 main
的调用者取回其寄存器窗口。 我不知道为什么它不在那里。 评论中的讨论讨论了 main
可能不需要弹出寄存器窗口,和/或您已将 main
声明为 void main()
(不保证与任何 C 实现一起使用,除非其文档明确指出,而且是总是不好的风格)...但是在 SPARC 上推送而不弹出寄存器窗口是一件很麻烦的事情,我也找不到解释令人信服。 我什至可以称其为编译器错误。
程序集调用
printf
,传递文本缓冲区和堆栈上的数字 20(这是您以迂回方式要求的)。