1_HelloWorld-1_汇编指示_-2

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中再返回。