Hello World - 1
Hello World
我们先从 C 语言入手来分析汇编代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Hello world\n");
exit(0);
return 0;
}
命令行输入 : gcc -S -O2 helloworld.c
生成 helloworld.s, 如果是 64 位系统就加上 -m32
参数生成 32 位汇编代码
我们可以用大 O 参数来设置优化级别, 这里是 2
生成的汇编代码如下 :
.file "helloworld.c"
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "Hello world\0"
.section .text.startup,"x"
.p2align 4,,15
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
call ___main
movl $LC0, (%esp)
call _puts
movl $0, (%esp)
call _exit
.cfi_endproc
LFE0:
.ident "GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
.def _puts; .scl 2; .type 32; .endef
.def _exit; .scl 2; .type 32; .endef
这个是我编译出来的代码, 下面是视频里面的代码 32 位 3.4.4 版本:
.file "helloworld.c"
.def __main; .scl 2; .type 32; .endef
.section .rdata, "dr"
LC0:
.ascii "Hello world\12\0"
.text
.p2align 4,,15
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl $16, %eax ; ? ?
movl %esp, %ebp ; 设置栈帧
subl $8, %esp ; 分配 8 字节栈空间
andl $-16, %esp ; 使栈顶地址 16 字节对齐
call __alloca ; C 运行时库初始化相关
call ____main ; C 运行时库初始化相关
movl $LC0, (%esp) ; 设置过程调用参数, LC0 为字符串地址
call _puts ; 因为我们没有进行格式化输出, 所以编译器这里调用 puts 函数
movl $0, (%esp) ; 设置过程调用参数
call _exit
; return 0 就没有体现
.def _puts; .scl 2; .type 32; .endef
.def _exit; .scl 2; .type 32; .endef
汇编指示
我们分析上面的程序代码
这里说一下, 在汇编语言中, 以 .
开头的行是汇编的指示 (Directives), 比如 .file
, .def
, .text
等, 用来指导汇编器如何进行汇编.
其中 .file
, .def
用于调试 (可以将其忽略)
以冒号 :
结尾的字符串 (如 _main
) 是用来表示变量或者函数的地址的符号 (Symbol). 其它均为汇编指令.
示例 : .globl _main
用于指示汇编器, 告诉它符号 _main
是全局的, 这样同一个程序的其它模块可以引用它.
LC0
则不是全局可见的.
继续分析上面的程序
.text
代码段, 告诉汇编器这里是代码.
后面这个 .p2align 4,,15
就有点奇怪了, 它用来指明后面的代码, 或者说有时候指的是数据, 它的对齐方式是什么. 就是我代码或者数据一行行地往下放, 但是纯粹连续放也有问题, 因为不同数据类型都有不同的对齐方式, 这里就是显式地指明了对齐方式 : 第 1 参数表示按照 2 的多少次幂字节对齐, 这里面是 2 的 4 次方, 就是按照 16 字节对齐; 第 2 参数表示对齐时额外空间用什么数据来填充, 一般来说默认是 0, 默认是 0 就不写了; 第 3 参数表示最多允许额外填充多少字节.
再下面就是 .section .rdata, "dr"
, 这说明从这里开始是一个数据段, 一个 readonly 只读的数据段. 接下来我们在只读数据段里面声明了一个类型为 ascii 字符串类型的数据 : LC0: .ascii "Hello world\12\0"
. 这个数据起始地址为
LC0
Hello World - 2
再回头看看这个程序
.file "helloworld.c" # 文件名
.def __main; .scl 2; .type 32; .endef # 调试相关的, 我们不去管它
.section .rdata, "dr" # 意思是说下面开始是只读数据段
LC0:
.ascii "Hello world\12\0"
.text # 再往下就是代码段了
.p2align 4,,15 # 给出我这个代码段要 16 字节对齐, 如果需要填充的数据超过 15 字节, 则不填充
.globl _main # 全局可见
.def _main; .scl 2; .type 32; .endef
_main: # main 函数入口
pushl %ebp
movl $16, %eax ; ? ?
movl %esp, %ebp ; 设置栈帧
subl $8, %esp ; 分配 8 字节栈空间
andl $-16, %esp ; 使栈顶地址 16 字节对齐
call __alloca ; C 运行时库初始化相关
call ____main ; C 运行时库初始化相关
movl $LC0, (%esp) ; 设置过程调用参数, LC0 为字符串地址
call _puts ; 因为我们没有进行格式化输出, 所以编译器这里调用 puts 函数
movl $0, (%esp) ; 设置过程调用参数
call _exit
; return 0 就没有体现
.def _puts; .scl 2; .type 32; .endef
.def _exit; .scl 2; .type 32; .endef
C 程序的内存布局
刚才说的程序的代码段数据段什么的, 实际上和它运行时在内存当中的布局有关
char big_array[1<<24]; /* 16 MB */
char huge_array[1<<28]; /* 256 MB */
int beyond;
char *p1, *p2, *p3, *p4;
int useless(){ return 0; }
int main()
{
p1 = malloc(1 << 28); /* 256 MB */
p2 = malloc(1 << 8); /* 256 B */
p3 = malloc(1 << 28); /* 256 MB */
p4 = malloc(1 << 8); /* 256 B */
/* Some print statement ... */
}
除了一些全局声明的一些数据, 比如说我们这里的 char int char* 声明的一些全局变量, 除了这些, 我们在 main 里面动态地分配了一些数据块.
我们来看看, 程序在运行起来之后, 我们所看到的代码, 它里面所分配的那些数据, 那些数据包括 main 动态声明出来的, 以及我在写代码静态声明的数据, 就是这些个数据在内存里面是怎么存放的, 大概地址的相互位置是什么关系.
首先 text 代码段应该放在比较靠下的位置, 但它不是最下面, 最下面还有一块, 一般 C 语言的有些函数放在这里.
text 往上就是数据段, 实际上数据段分很多种, 刚才我们看过了只读数据段, 后面会讲到可读可写的一般数据段, 还有一些全局数据段 (没有初始化过的那种). 就相当于你在 C 里面声明的一些全局变量, 静态变量都会放在数据段里面.
再往上呢就是堆 (Heap). 堆是什么意思呢, 刚才看到的那些 malloc
动态分配出来的那些数据就放在堆里面.
堆是自下往上长, 栈是自顶往下降.
局部变量放在栈里面.
文字填空题
在X86-32位编程中有一种简单的获得所运行的指令地址的方法(X86-32位结构下eip寄存器是无法直接访问的)。比如说我们要获得下面程序中XXXX这条指令的地址并置于eax寄存器中,那么可以采用如下代码段。请补充完函数GetAddress的第一条语句(AT&T语法)。
movl ____, ____
ret
call GetAddress
xxxx
答案:(%esp)
;%eax
分析:在 call
之后,GetAddress的返回地址,也就是xxxx的地址被压栈。此时,只需将栈顶所指向位置的内容((%esp)
),注意括号代表访存)存入eax中再返回。