#include<stdio.h>
int main()
{
printf("hello,world\n");
return 0;
}
x86
使用MSVC编译程序
cl 1.cpp /Fa 1.asm
/Fa选项将使编译器生成汇编指令清单文件(assembly listing file),并指定汇编列表文件的文件名称是1.asm
1.asm内容如下:
CONST SEGMENT
$SG3830 DB ‘hello,world',0AH,00H
CONST END
PUBLIC _main
EXTRN _printf:PROC;Function compile flags:/0dtp
_TEXT SEGMENT
_main PROC
push ebp
mov ebp,esp
push OFFSET $SG3830
call _printf
add esp,4
xor eax,eax
pop ebp
ret 0
_main ENDP
_TEXT ENDS
在生成1.asm后,编译器会生成1.obj再将之链接为可执行文件1.exe
CONST:数据段
_TEXT:代码段
上述源码第等效于:
#include <stdio.h>
const char *$SG3830[] = "hello,world\n";
int main()
{
printf($SG3830);
return 0;
}
我们发现编译器在字符串常量的尾部添加了十六进制的数字0,即00h,为字符串常量添加结束标志。
通过PUSH指令,程序把字符串的指针推送入栈。这样,printf()函数就可以调用栈里的指针,即字符串“hello,world\n"的地址 。
在printf()函数结束以后,程序的控制流会返回到main()函数之中。此时,字符串地址仍残留在数据栈之中。这时候就需要调整指针ESP寄存器里的值来释放这个指针。
add ESP,4把ESP寄存器里的数值加4
为什么要加上4,这是因为x86平台的内存地址使用32位数据描述。同理,在x64系统上释放这个指针时,ESP就要加上8.
因此,这条指令可以理解为POP某寄存器。只是本例的指令直接舍弃了栈里的数据而POP指令还要把寄存器里的值存储到既定寄存器。
printf()函数结束之后,main()函数会返回0。即main()函数的运算结果是0.
这个返回值是由指令XOR EAX,EAX计算出来的。
gcc生成 hello world程序
gcc 1.c -o 1
汇编指令
Main proc near
var_10 = dword ptr -10h
push ebp
mov ebp,esp
and esp,0FFFFFFF0h
sub esb,10h
mov eax,offsett aHelloWorld; "hello,world\n"
mov [esp+10h+var_10],eax
call _printf
mov eax,0
leave
retn
main endp
AND
ESP,0FFFFFFF0h指令,它令栈地址ESP的值向16字节边对齐,成为16的整数倍,属于初始化指令。如果地址位没有对齐,那么CPU可能需要访问两次内存才能获得栈内数据。虽然在8字节边界对齐就可以满足32位x86
CPU和64位x64 CPU的要求,但是主流编译器的编译规则规定”程序访问的地址必须向16字节对齐“。
SUB ESP,10h 将在栈中分配0x10
bytes,即16字节。该程序只会用到4字节空间。但是因为编译器对栈地址ESP进行了16字节对齐,所以每次都会分配16字节的空间。
而后,程序将字符串地址直接写入到数据栈。其中var_10是局部变量,用来向后面的printf()函数传递参数。
最后一条LEAVE指令,等效于MOV ESP,EBP 和POP EBP两条指令。
GCC的其他特性
#include<stdio.h>
int f1()
{
printf("world\n");
}
int f2()
{
printf("hello world\n");
}
int main()
{
f1();
f2();
}
汇编指令
f1 proc near
s =dowrd ptr-1ch
sub esp,1Ch
mov [esp+1Ch+s],offset s; "world\n"
call _puts
add esp,1Ch
retn
f1 endp
f2 proc near
s =dword ptr-1ch
sub esp,1Ch
mov [esp+1Ch+s],offset aHello;"hello ”
call _puts
add esp,1Ch
retn
f2 endp
aHello db 'hello'
s db 'world',0xa,0
在打印字符串“hello
world"的时候,这两个词指针地址实际上是前后相邻的。在调用puts()函数进行输出时,函数本身不知道它所输出的字符串分为两个部分。实际上我们在汇编指令清单中可以看到,这两个字符串没有被切实分开。
在f1()函数调用
puts函数时,它输出字符串”world"和外加结束符,因为puts()函数并不知道字符串可以和前面的字符串连起来形成新的字符串。GCC会充分用这种技术来节省内存。
ARM
未启用优化功能的ARM模式
armcc.exe --arm --c90 -O 0 1.c
main
STMFD SP!,{R4,LR}
ADR R0,aHelloWorld; "hello, world"
BL __2printf
MOV R0,#0
LDMFD SP!,{R4,PC}
aHelloWorld DCB "hello,world",0
STMFD SP!,{R4,LR} 相当于x86r的PUSH指令。它把R4寄存器和LR Link
Register寄存器的数值放到数据栈中。这条指令首先将SP递减,在栈中分配一个新的空间以便存储R4和LR的值。
ADR R0,aHelloWorld 它首先对PC进行取值操作,然后把“hello,world"字符串的偏移量与PC的值相加,将其结果存储到R0之中。
BL __2printf调用printf()函数。BL具体操作:
1)将下一条指令的地址,即地址0xC处 MOV R0,#0的地址,写入LR寄存器
2)然后将printf()函数的地址 写入PC寄存器,以引导系统执行该函数
MOV R0,#0 将R0寄存器置0
LDMFD SP!,R4,PC这和条指令。它将栈中的数值取出,依次赋值给R4和PC,并且会调整栈指针SP。