#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。

 

 

技术
下载桌面版
GitHub
Gitee
SourceForge
百度网盘(提取码:draw)
云服务器优惠
华为云优惠券
腾讯云优惠券
阿里云优惠券
Vultr优惠券
站点信息
问题反馈
邮箱:[email protected]
吐槽一下
QQ群:766591547
关注微信