计算机那些事(3)——程序构建及编译原理

本文深入讲解了程序构建的四个核心步骤:预处理、编译、汇编和链接,通过HelloWorld程序实例,详细解释了每一步的具体操作及涉及的关键概念。

最近在看《程序员的自我修养——链接、装载与库》一书,这本书以前看过一部分,由于难啃,当时没有坚持下去。现在工作了,每天接触的都是业务开发,对底层的一些东西感觉越来越陌生。于是,又把此书翻了出来拜读。为了加深阅读的印象,打算对书中的一些有价值的内容进行整理,也方便后续回顾。

程序构建流程

下面以“Hello World”程序为例,来介绍程序的编译与链接过程。

 
// hello.c
#include <stdio.h>

int main() {
    printf("Hello World!\n");
    return 0;
}

在Linux下,可以直接使用GCC来编译Hello World程序:

 
$ gcc hello.c
$ ./a.out
Hello World!

GCC编译命令隐藏了构建过程中的一些复杂的步骤,主要有4个步骤,如下图所示。

  • 预处理(Propressing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)

预编译

预编译步骤将源代码文件hello.c以及相关头文件,如:stdio.h等预编译生成一个.i文件。对于C++程序,其源代码文件的扩展名可能是.cpp或.cxx,头文件的扩展名可能是.hpp,预编译生成.ii文件。

预编译步骤相当于执行如下命令(选项-E表示只进行预编译)

 
$ gcc -E hello.c -o hello.i

 

 
$ cpp hello.c > hello.i

 

预编译主要处理源代码中的以“#”开始的预编译指令,如:“#include”、“#define”等,其主要处理规则如下:

  • 将所有的“#define”删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,如:“#if”、“#ifdef”、“#else”、“#endif”。
  • 处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。该过程是递归进行的,因为被包含的文件可能还包含其他文件。
  • 删除所有的注释“//”和“/ /”。
  • 添加行号和文件名标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试试用的行号信息以及用于编译时产生编译错误或警告时能够显示行号。
  • 保留所有的#pragma编译器指令,因为编译器须要试用他们。

预编译生成的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 367 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
# 410 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 411 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 368 "/usr/include/features.h" 2 3 4
# 391 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4
# 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4
# 392 "/usr/include/features.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4

# 1 "/usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h" 1 3 4
# 216 "/usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h" 3 4

# 216 "/usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h" 3 4
typedef long unsigned int size_t;
# 34 "/usr/include/stdio.h" 2 3 4

# 1 "/usr/include/x86_64-linux-gnu/bits/types.h" 1 3 4
# 27 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 28 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4


typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;

typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef signed short int __int16_t;
typedef unsigned short int __uint16_t;
typedef signed int __int32_t;
typedef unsigned int __uint32_t;

typedef signed long int __int64_t;
typedef unsigned long int __uint64_t;

typedef long int __quad_t;
typedef unsigned long int __u_quad_t;
# 121 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/typesizes.h" 1 3 4
# 122 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4

typedef unsigned long int __dev_t;
typedef unsigned int __uid_t;
typedef unsigned int __gid_t;
typedef unsigned long int __ino_t;
typedef unsigned long int __ino64_t;
typedef unsigned int __mode_t;
typedef unsigned long int __nlink_t;
typedef long int __off_t;
typedef long int __off64_t;
typedef int __pid_t;
typedef struct { int __val[2]; } __fsid_t;
typedef long int __clock_t;
typedef unsigned long int __rlim_t;
typedef unsigned long int __rlim64_t;
typedef unsigned int __id_t;
typedef long int __time_t;
typedef unsigned int __useconds_t;
typedef long int __suseconds_t;

typedef int __daddr_t;
typedef int __key_t;

typedef int __clockid_t;

typedef void * __timer_t;

typedef long int __blksize_t;

typedef long int __blkcnt_t;
typedef long int __blkcnt64_t;


typedef unsigned long int __fsblkcnt_t;
typedef unsigned long int __fsblkcnt64_t;

typedef unsigned long int __fsfilcnt_t;
typedef unsigned long int __fsfilcnt64_t;

typedef long int __fsword_t;

typedef long int __ssize_t;


typedef long int __syscall_slong_t;

typedef unsigned long int __syscall_ulong_t;

typedef __off64_t __loff_t;
typedef __quad_t *__qaddr_t;
typedef char *__caddr_t;


typedef long int __intptr_t;


typedef unsigned int __socklen_t;
# 36 "/usr/include/stdio.h" 2 3 4
# 44 "/usr/include/stdio.h" 3 4
struct _IO_FILE;

typedef struct _IO_FILE FILE;

# 64 "/usr/include/stdio.h" 3 4
typedef struct _IO_FILE __FILE;
# 74 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/libio.h" 1 3 4
# 31 "/usr/include/libio.h" 3 4
# 1 "/usr/include/_G_config.h" 1 3 4
# 15 "/usr/include/_G_config.h" 3 4
# 1 "/usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h" 1 3 4
# 16 "/usr/include/_G_config.h" 2 3 4


# 1 "/usr/include/wchar.h" 1 3 4
# 82 "/usr/include/wchar.h" 3 4
typedef struct
{
  int __count;
  union
  {

    unsigned int __wch;



    char __wchb[4];
  } __value;
} __mbstate_t;
# 21 "/usr/include/_G_config.h" 2 3 4
typedef struct
{
  __off_t __pos;
  __mbstate_t __state;
} _G_fpos_t;
typedef struct
{
  __off64_t __pos;
  __mbstate_t __state;
} _G_fpos64_t;
# 32 "/usr/include/libio.h" 2 3 4
# 49 "/usr/include/libio.h" 3 4
# 1 "/usr/lib/gcc/x86_64-linux-gnu/5/include/stdarg.h" 1 3 4
# 40 "/usr/lib/gcc/x86_64-linux-gnu/5/include/stdarg.h" 3 4
typedef __builtin_va_list __gnuc_va_list;
# 50 "/usr/include/libio.h" 2 3 4
# 144 "/usr/include/libio.h" 3 4
struct _IO_jump_t; struct _IO_FILE;


typedef void _IO_lock_t;


struct _IO_marker {
  struct _IO_marker *_next;
  struct _IO_FILE *_sbuf;

  int _pos;
# 173 "/usr/include/libio.h" 3 4
};


enum __codecvt_result
{
  __codecvt_ok,
  __codecvt_partial,
  __codecvt_error,
  __codecvt_noconv
};
# 241 "/usr/include/libio.h" 3 4
struct _IO_FILE {
  int _flags;

  char* _IO_read_ptr;
  char* _IO_read_end;
  char* _IO_read_base;
  char* _IO_write_base;
  char* _IO_write_ptr;
  char* _IO_write_end;
  char* _IO_buf_base;
  char* _IO_buf_end;

  char *_IO_save_base;
  char *_IO_backup_base;
  char *_IO_save_end;

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;

  int _flags2;

  __off_t _old_offset;



  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];



  _IO_lock_t *_lock;
# 289 "/usr/include/libio.h" 3 4
  __off64_t _offset;


  void *__pad1;
  void *__pad2;
  void *__pad3;
  void *__pad4;

  size_t __pad5;
  int _mode;

  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];

};


typedef struct _IO_FILE _IO_FILE;


struct _IO_FILE_plus;

extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
# 333 "/usr/include/libio.h" 3 4
typedef __ssize_t __io_read_fn (void *__cookie, char *__buf, size_t __nbytes);


typedef __ssize_t __io_write_fn (void *__cookie, const char *__buf,
     size_t __n);


typedef int __io_seek_fn (void *__cookie, __off64_t *__pos, int __w);

typedef int __io_close_fn (void *__cookie);
# 385 "/usr/include/libio.h" 3 4
extern int __underflow (_IO_FILE *);
extern int __uflow (_IO_FILE *);
extern int __overflow (_IO_FILE *, int);
# 429 "/usr/include/libio.h" 3 4
extern int _IO_getc (_IO_FILE *__fp);
extern int _IO_putc (int __c, _IO_FILE *__fp);
extern int _IO_feof (_IO_FILE *__fp) __attribute__ ((__nothrow__ , __leaf__));
extern int _IO_ferror (_IO_FILE *__fp) __attribute__ ((__nothrow__ , __leaf__));

extern int _IO_peekc_locked (_IO_FILE *__fp);

extern void _IO_flockfile (_IO_FILE *) __attribute__ ((__nothrow__ , __leaf__));
extern void _IO_funlockfile (_IO_FILE *) __attribute__ ((__nothrow__ , __leaf__));
extern int _IO_ftrylockfile (_IO_FILE *) __attribute__ ((__nothrow__ , __leaf__));
# 459 "/usr/include/libio.h" 3 4
extern int _IO_vfscanf (_IO_FILE * __restrict, const char * __restrict,
   __gnuc_va_list, int *__restrict);
extern int _IO_vfprintf (_IO_FILE *__restrict, const char *__restrict,
    __gnuc_va_list);
extern __ssize_t _IO_padn (_IO_FILE *, int, __ssize_t);
extern size_t _IO_sgetn (_IO_FILE *, void *, size_t);

extern __off64_t _IO_seekoff (_IO_FILE *, __off64_t, int, int);
extern __off64_t _IO_seekpos (_IO_FILE *, __off64_t, int);

extern void _IO_free_backup_area (_IO_FILE *) __attribute__ ((__nothrow__ , __leaf__));
# 75 "/usr/include/stdio.h" 2 3 4




typedef __gnuc_va_list va_list;
# 90 "/usr/include/stdio.h" 3 4
typedef __off_t off_t;
# 102 "/usr/include/stdio.h" 3 4
typedef __ssize_t ssize_t;

typedef _G_fpos_t fpos_t;
# 164 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/stdio_lim.h" 1 3 4
# 165 "/usr/include/stdio.h" 2 3 4



extern struct _IO_FILE *stdin;
extern struct _IO_FILE *stdout;
extern struct _IO_FILE *stderr;


extern int remove (const char *__filename) __attribute__ ((__nothrow__ , __leaf__));

extern int rename (const char *__old, const char *__new) __attribute__ ((__nothrow__ , __leaf__));

extern int renameat (int __oldfd, const char *__old, int __newfd,
       const char *__new) __attribute__ ((__nothrow__ , __leaf__));


extern FILE *tmpfile (void) ;
# 209 "/usr/include/stdio.h" 3 4
extern char *tmpnam (char *__s) __attribute__ ((__nothrow__ , __leaf__)) ;

extern char *tmpnam_r (char *__s) __attribute__ ((__nothrow__ , __leaf__)) ;
# 227 "/usr/include/stdio.h" 3 4
extern char *tempnam (const char *__dir, const char *__pfx)
     __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__malloc__)) ;


extern int fclose (FILE *__stream);

extern int fflush (FILE *__stream);

# 252 "/usr/include/stdio.h" 3 4
extern int fflush_unlocked (FILE *__stream);
# 266 "/usr/include/stdio.h" 3 4

extern FILE *fopen (const char *__restrict __filename,
      const char *__restrict __modes) ;

extern FILE *freopen (const char *__restrict __filename,
        const char *__restrict __modes,
        FILE *__restrict __stream) ;
# 295 "/usr/include/stdio.h" 3 4

# 306 "/usr/include/stdio.h" 3 4
extern FILE *fdopen (int __fd, const char *__modes) __attribute__ ((__nothrow__ , __leaf__)) ;
# 319 "/usr/include/stdio.h" 3 4
extern FILE *fmemopen (void *__s, size_t __len, const char *__modes)
  __attribute__ ((__nothrow__ , __leaf__)) ;

extern FILE *open_memstream (char **__bufloc, size_t *__sizeloc) __attribute__ ((__nothrow__ , __leaf__)) ;

extern void setbuf (FILE *__restrict __stream, char *__restrict __buf) __attribute__ ((__nothrow__ , __leaf__));

extern int setvbuf (FILE *__restrict __stream, char *__restrict __buf,
      int __modes, size_t __n) __attribute__ ((__nothrow__ , __leaf__));

extern void setbuffer (FILE *__restrict __stream, char *__restrict __buf,
         size_t __size) __attribute__ ((__nothrow__ , __leaf__));

extern void setlinebuf (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));

extern int fprintf (FILE *__restrict __stream,
      const char *__restrict __format, ...);

extern int printf (const char *__restrict __format, ...);

extern int sprintf (char *__restrict __s,
      const char *__restrict __format, ...) __attribute__ ((__nothrow__));


extern int vfprintf (FILE *__restrict __s, const char *__restrict __format,
       __gnuc_va_list __arg);

extern int vprintf (const char *__restrict __format, __gnuc_va_list __arg);

extern int vsprintf (char *__restrict __s, const char *__restrict __format,
       __gnuc_va_list __arg) __attribute__ ((__nothrow__));

extern int snprintf (char *__restrict __s, size_t __maxlen,
       const char *__restrict __format, ...)
     __attribute__ ((__nothrow__)) __attribute__ ((__format__ (__printf__, 3, 4)));

extern int vsnprintf (char *__restrict __s, size_t __maxlen,
        const char *__restrict __format, __gnuc_va_list __arg)
     __attribute__ ((__nothrow__)) __attribute__ ((__format__ (__printf__, 3, 0)));

# 412 "/usr/include/stdio.h" 3 4
extern int vdprintf (int __fd, const char *__restrict __fmt,
       __gnuc_va_list __arg)
     __attribute__ ((__format__ (__printf__, 2, 0)));
extern int dprintf (int __fd, const char *__restrict __fmt, ...)
     __attribute__ ((__format__ (__printf__, 2, 3)));


extern int fscanf (FILE *__restrict __stream,
     const char *__restrict __format, ...) ;
extern int scanf (const char *__restrict __format, ...) ;

extern int sscanf (const char *__restrict __s,
     const char *__restrict __format, ...) __attribute__ ((__nothrow__ , __leaf__));
# 443 "/usr/include/stdio.h" 3 4
extern int fscanf (FILE *__restrict __stream, const char *__restrict __format, ...) __asm__ ("" "__isoc99_fscanf")

                               ;
extern int scanf (const char *__restrict __format, ...) __asm__ ("" "__isoc99_scanf")
                              ;
extern int sscanf (const char *__restrict __s, const char *__restrict __format, ...) __asm__ ("" "__isoc99_sscanf") __attribute__ ((__nothrow__ , __leaf__))

                      ;
# 463 "/usr/include/stdio.h" 3 4


extern int vfscanf (FILE *__restrict __s, const char *__restrict __format,
      __gnuc_va_list __arg)
     __attribute__ ((__format__ (__scanf__, 2, 0))) ;

extern int vscanf (const char *__restrict __format, __gnuc_va_list __arg)
     __attribute__ ((__format__ (__scanf__, 1, 0))) ;


extern int vsscanf (const char *__restrict __s,
      const char *__restrict __format, __gnuc_va_list __arg)
     __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__format__ (__scanf__, 2, 0)));
# 494 "/usr/include/stdio.h" 3 4
extern int vfscanf (FILE *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg) __asm__ ("" "__isoc99_vfscanf")



     __attribute__ ((__format__ (__scanf__, 2, 0))) ;
extern int vscanf (const char *__restrict __format, __gnuc_va_list __arg) __asm__ ("" "__isoc99_vscanf")

     __attribute__ ((__format__ (__scanf__, 1, 0))) ;
extern int vsscanf (const char *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg) __asm__ ("" "__isoc99_vsscanf") __attribute__ ((__nothrow__ , __leaf__))

     __attribute__ ((__format__ (__scanf__, 2, 0)));
# 522 "/usr/include/stdio.h" 3 4


extern int fgetc (FILE *__stream);
extern int getc (FILE *__stream);

extern int getchar (void);

# 550 "/usr/include/stdio.h" 3 4
extern int getc_unlocked (FILE *__stream);
extern int getchar_unlocked (void);
# 561 "/usr/include/stdio.h" 3 4
extern int fgetc_unlocked (FILE *__stream);




extern int fputc (int __c, FILE *__stream);
extern int putc (int __c, FILE *__stream);

extern int putchar (int __c);

# 594 "/usr/include/stdio.h" 3 4
extern int fputc_unlocked (int __c, FILE *__stream);


extern int putc_unlocked (int __c, FILE *__stream);
extern int putchar_unlocked (int __c);


extern int getw (FILE *__stream);

extern int putw (int __w, FILE *__stream);



extern char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
     ;
# 640 "/usr/include/stdio.h" 3 4

# 665 "/usr/include/stdio.h" 3 4
extern __ssize_t __getdelim (char **__restrict __lineptr,
          size_t *__restrict __n, int __delimiter,
          FILE *__restrict __stream) ;
extern __ssize_t getdelim (char **__restrict __lineptr,
        size_t *__restrict __n, int __delimiter,
        FILE *__restrict __stream) ;


extern __ssize_t getline (char **__restrict __lineptr,
       size_t *__restrict __n,
       FILE *__restrict __stream) ;

extern int fputs (const char *__restrict __s, FILE *__restrict __stream);

extern int puts (const char *__s);

extern int ungetc (int __c, FILE *__stream);


extern size_t fread (void *__restrict __ptr, size_t __size,
       size_t __n, FILE *__restrict __stream) ;



extern size_t fwrite (const void *__restrict __ptr, size_t __size,
        size_t __n, FILE *__restrict __s);

# 737 "/usr/include/stdio.h" 3 4
extern size_t fread_unlocked (void *__restrict __ptr, size_t __size,
         size_t __n, FILE *__restrict __stream) ;
extern size_t fwrite_unlocked (const void *__restrict __ptr, size_t __size,
          size_t __n, FILE *__restrict __stream);


extern int fseek (FILE *__stream, long int __off, int __whence);

extern long int ftell (FILE *__stream) ;

extern void rewind (FILE *__stream);

# 773 "/usr/include/stdio.h" 3 4
extern int fseeko (FILE *__stream, __off_t __off, int __whence);

extern __off_t ftello (FILE *__stream) ;
# 792 "/usr/include/stdio.h" 3 4


extern int fgetpos (FILE *__restrict __stream, fpos_t *__restrict __pos);

extern int fsetpos (FILE *__stream, const fpos_t *__pos);
# 815 "/usr/include/stdio.h" 3 4

# 824 "/usr/include/stdio.h" 3 4

extern void clearerr (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));

extern int feof (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;

extern int ferror (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;




extern void clearerr_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int feof_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern int ferror_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;


extern void perror (const char *__s);

# 1 "/usr/include/x86_64-linux-gnu/bits/sys_errlist.h" 1 3 4
# 26 "/usr/include/x86_64-linux-gnu/bits/sys_errlist.h" 3 4
extern int sys_nerr;
extern const char *const sys_errlist[];
# 854 "/usr/include/stdio.h" 2 3 4

extern int fileno (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;

extern int fileno_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
# 872 "/usr/include/stdio.h" 3 4
extern FILE *popen (const char *__command, const char *__modes) ;


extern int pclose (FILE *__stream)


extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
# 912 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));



extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;


extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 942 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2

# 2 "hello.c"
int main(){

printf("hello!\n");


return 0;

}

编译

编译就是把预处理生成的文件进行一系列词法分析、语法分析、语义分析、优化,生成相应的汇编代码文件。这个过程是整个程序构建的核心部分,也是最复杂的部分之一。

编译步骤相当于执行如下命令:

1
$ gcc -S hello.i -o hello.s

1
$ gcc -S hello.c -o hello.s

现在版本的GCC把预编译和编译两个步骤合并成了一个步骤,使用一个叫cc1的程序来完成。该程序位于“/usr/lib/gcc/x86_64-linux-gnu/4.8/”,我们可以直接调用cc1来完成它:

1
$ /usr/lib/gcc/x86_64-linux-gnu/4.8/cc1 hello.c

事实上,对于不同的语言,预编译与编译的程序是不同的,如下所示:

  • C:cc1
  • C++:cc1plus
  • Objective-C:cc1obj
  • Fortran:f771
  • Java:jc1

GCC是对这些后台程序的封装,它会根据不同的参数来调用预编译程序cc1、汇编器as、链接器ld。

汇编

汇编就是将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。汇编过程相对于编译比较简单,其没有复杂的语法、语义,也无需做指令优化,只是根据汇编指令和机器指令的对照表进行翻译。

汇编步骤相当执行如下命令:

1
$ gcc -c hello.s -o hello.o

 

1
$ gcc -c hello.c -o hello.o

 

GCC本质上是调用汇编器as来完成汇编步骤的,我们可以直接调用as来完成该步骤:

1
$ as hello.s -o hello.o

 

链接

链接主要是将前面步骤生成多个目标文件进行重定位等复杂的操作,从而生成可执行文件。链接可分为静态链接和动态链接。

编译器工作原理

编译过程可以分为6个步骤,如下图所示。

  • 扫描(Scanning)(又称词法分析)
  • 语法分析(Syntax analysis)
  • 语义分析(Semantic Analysis)
  • 源代码优化(Source Code Optimization)
  • 目标代码生成(Target Code Generation)
  • 目标代码优化(Target Code Optimization)

下面我们以一行简单的C语言代码为例,简单描述从源代码(Source Code)最终目标代码的过程。代码示例如下:

1
2
// CompilerExpression.c
array[index] = (index + 4) * (2 + 6)

 

扫描(词法分析)

首先源代码被输入到扫描器(Scanner),扫描器的任务很简单,只是简单地进行词法分析,运用一种类似于有限状态机(Finite State Machine)的算法将源代码的字符序列分割成一系列的记号(Token)

以上述代码为例,总共包含了28个非空字符,经过扫描后,产生了16个记号。

记号类型
array标识符
[左方括号
index标识符
[右方括号
=赋值
(左圆括号
index标识符
+加号
4数字
)右圆括号
*乘号
(左圆括号
2数字
+加号
6数字
)右圆括号

词法分析产生的记号一般可以分为一下几类:关键字字面量(包含数字、字符串等)和特殊符号(如加号、等号)。

在识别记号的同时,扫描器也完成了其他工作。如:将标识符存放到符号表,将数字、字符串常量存放到文字表等,以备后面的步骤使用。

有一个名为lex的程序可以实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。正因为有这样一个程序存在,编译器的开发者就无需为每个编译器开发一个独立的词法扫描器,而是根据需要改变词法规则即可。

语法分析

语法分析器(Grammar Parser)将对由扫描器产生的记号进行语法分析。从而产生语法树(Syntax Tree)。整个分析过程采用了上下文无关语法(Context-freeGrammar)的分析手段。简单地讲,由语法分析器生成的语法树是以表达式(Expression)为节点的树。

以上述代码为例,其中的语句就是一个由赋值表达式、加法表达式、乘法表达式、数组表达式、括号表达式组成的复杂语句,下图所示为该语句经过语法分析器后生成的语法树。

1
2
// CompilerExpression.c
array[index] = (index + 4) * (2 + 6)

 

在语法分析的同时,很多运算符号的优先级和含义也被确定下来了。如:乘法表达式的优先级比加法高,圆括号表达式的优先级比乘法高,等等。另外,有些符号具有多重含义,如“*”在C语言中可以表示乘法表达式,也可以表示对指针取内容的表达式,因此语法分析阶段必须对这些内容进行区分。如果出现了表达式不合法,如各种括号不匹配、表达式中缺少操作符等,编译器就会报告语法分析阶段的错误。

有一个名为yacc(Yet Another Compiler Compiler)的工具可以实现语法分析。其根据用户给定的语法规则对输入的记号序列进行解析,从而构建出语法树。对于不同的编程语言,编译器的开发者只需改变语法规则,而无需为每个编译器编写一个语法分析器。因此,其也称为“编译器编译器(Compiler Compiler)”

语义分析

语法分析仅仅完成了对表达式的语法层面的分析,但它并不了解这个语句的真正含义,如:C语言里两个指针做乘法运算是没有意义的,但这个语句在语法上是合法的。编译器所能分析的语义是静态语义(Static Semantic),所谓静态语义是指在编译期间可以确定的语义,与之对应的动态语义(Dynamic Semantic)就是只有在运行期才能确定的语义。

静态语义通常包括声明和类型的匹配,类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型的转换过程,语义分析过程中需要完成该步骤。比如讲一个浮点赋值给一个指针时,语义分析程序会发现这个类型不匹配,编译器将会报错。动态语义一般是指在运行期出现的语义相关的问题,比如将0作为除数是一个运行期语义错误。

经过语义分析阶段之后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。下图所示为标记语义后的语法树。

源代码优化(中间代码生成)

现代编译器有着很多层次的优化,源码优化器(Source Code Optimizer)则是在源代码级别进行优化。上述例子中,(2 + 6)这个表达式可以被优化掉。因为它的值在编译期就可以被确定。下图所示为优化后的语法树。

事实上,直接在语法树上作优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码(Intermediate Code),它是语法树的顺序表示,其实它已经非常接近目标代码了。但它一般与目标机器和运行时环境是无关的,比如它不包含数据的尺寸、变量地址和寄存器的名字等。

中间代码有很多种类型,在不同的编译器中有着不同的形式,比较常见的有:三地址码(Three-address Code)P-代码(P-Code)。以三地址码为例,最基本的三地址码如下所示:

1
2
x = y op z
# 表示将变量y和z进行op操作后,赋值给x。

 

因此,可以将上述例子的代码翻译成三地址码:

1
2
3
4
t1 = 2 + 6
t2 = index + 4
t3 = t2 * t1
array[index] = t3

 

为了使所有的操作符合三地址码形式,这里使用了几个临时变量:t1、t2和t3。在三地址码的基础上进行优化时,优化程序会将2+6的结果计算出来,得到t1 = 6。因此,进一步优化后可以得到如下的代码:

1
2
3
t2 = index + 4
t2 = t2 * 8
array[index] = t2

 

中间代码将编译器分为前端(Front End)后端(Back End)。编译器前端负责产生机器无关的中间代码,编译器后端负责将中间代码转换成目标机器代码。这样,对于一些可跨平台的编译器,它们可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端。比如clange就是一个前端工具,而LLVM则负责后端处理。GCC则是一个套装,包揽了前后端的所有任务。


目标代码生成

目标代码生成主要由代码生成器(Code Generator)完成。代码生成器将中间代码转换成目标机器代码,该过程十分依赖目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等。

上述例子的中间代码,经过代码生成器的处理之后可能会生成如下所示的代码序列(以x86汇编为例,假设index的类型为int型,array的类型为int型数组):

1
2
3
4
5
movl index, %ecx            ; value of index to ecx
addl $4, %ecx               ; ecx = ecx + 4
mull $8, %ecx               ; ecx = ecx * 8
movl index, %eax            ; value of index to eax
movl %ecx, array(,%eax,4)    ; array[index] = ecx

 

目标代码优化

目标代码生成后,由目标代码优化器(Target Code Optimizer)来进行优化。比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等。

上述例子中,乘法由一条相对复杂的基址比例变址寻址(Base Index Scale Addressing)的lea指令完成,随后由一条mov指令完成最后的赋值操作,这条mov指令的寻址方式与lea是一样的。如下所示为优化后的目标代码:

1
2
3
movl index, %edx
leal 32(,%edx,8), %eax
movl %eax, array(,%edx,4)

 

结尾

经过扫描、语法分析、语义分析、源代码优化、目标代码生成、目标代码优化等一系列步骤之后,源代码终于被编译成了目标代码。但是这个目标代码中有一个问题:

index和array的地址还没有确定

如果我们把目标代码使用汇编器编译成真正能够在机器上运行的指令,那么index和array的地址来自哪里?
如果index和array定义在跟上面的源代码同一个编译单元里,那么编译器可以为index和array分配空间,确定地址;但如果是定义在其他的程序模块呢?

事实上,定义其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所以现代编译器可以将一个源文件编译成一个未链接的目标文件,然后由编译器最终将这些目标文件链接起来形成可执行文件。

后面,我们将继而探讨链接的原理。

参考

  1. 《程序员的自我修养——链接、装载与库》

(完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值