导读 想象一下,在无法访问软件的源代码时,但仍然能够理解软件的实现方式,在其中找到漏洞,并且更厉害的是还能修复错误。所有这些都是在只有二进制文件时做到的。这听起来就像是超能力,对吧?你也可以拥有这样的超能力,GNU 二进制实用程序(binutils)就是一个很好的起点。GNU binutils 是一个二进制工具集,默认情况下所有 Linux 发行版中都会安装这些二进制工具。

二进制分析是计算机行业中最被低估的技能。它主要由恶意软件分析师、反向工程师和使用底层软件的人使用。

本文探讨了 binutils 可用的一些工具。我使用的是 RHEL,但是这些示例应该在任何 Linux 发行版上可以运行。

[~]# cat /etc/redhat-release 
Red Hat Enterprise Linux Server release 7.6 (Maipo)
[~]# 
[~]# uname -r
3.10.0-957.el7.x86_64
[~]# 

请注意,某些打包命令(例如 rpm)在基于 Debian 的发行版中可能不可用,因此请使用等效的 dpkg 命令替代。

软件开发的基础知识

在开源世界中,我们很多人都专注于源代码形式的软件。当软件的源代码随时可用时,很容易获得源代码的副本,打开喜欢的编辑器,喝杯咖啡,然后就可以开始探索了。

但是源代码不是在 CPU 上执行的代码,在 CPU 上执行的是二进制或者说是机器语言指令。二进制或可执行文件是编译源代码时获得的。熟练的调试人员深谙通常这种差异。

编译的基础知识

在深入研究 binutils 软件包本身之前,最好先了解编译的基础知识。

编译是将程序从某种编程语言(如 C/C++)的源代码(文本形式)转换为机器代码的过程。

机器代码是 CPU(或一般而言,硬件)可以理解的 1 和 0 的序列,因此可以由 CPU 执行或运行。该机器码以特定格式保存到文件,通常称为可执行文件或二进制文件。在 Linux(和使用 Linux 兼容二进制的 BSD)上,这称为 ELF(可执行和可链接格式Executable and Linkable Format)。

在生成给定的源文件的可执行文件或二进制文件之前,编译过程将经历一系列复杂的步骤。以这个源程序(C 代码)为例。打开你喜欢的编辑器,然后键入以下程序:

#include 
int main(void)
{
  printf("Hello World\n");
  return 0;
}
步骤 1:用 cpp 预处理

C 预处理程序(cpp)用于扩展所有宏并将头文件包含进来。在此示例中,头文件 stdio.h 将被包含在源代码中。stdio.h 是一个头文件,其中包含有关程序内使用的 printf 函数的信息。对源代码运行 cpp,其结果指令保存在名为 hello.i 的文件中。可以使用文本编辑器打开该文件以查看其内容。打印 “hello world” 的源代码在该文件的底部。

[testdir]# cat hello.c
#include 
int main(void)
{
  printf("Hello World\n");
  return 0;
}
[testdir]#
[testdir]# cpp hello.c > hello.i
[testdir]#
[testdir]# ls -lrt
total 24
-rw-r--r--. 1 root root 76 Sep 13 03:20 hello.c
-rw-r--r--. 1 root root 16877 Sep 13 03:22 hello.i
[testdir]#
步骤 2:用 gcc 编译

在此阶段,无需创建目标文件就将步骤 1 中生成的预处理源代码转换为汇编语言指令。这个阶段使用 GNU 编译器集合(gcc)。对 hello.i 文件运行带有 -S 选项的 gcc 命令后,它将创建一个名为 hello.s 的新文件。该文件包含该 C 程序的汇编语言指令。

你可以使用任何编辑器或 cat 命令查看其内容。

[testdir]#
[testdir]# gcc -Wall -S hello.i
[testdir]#
[testdir]# ls -l
total 28
-rw-r--r--. 1 root root 76 Sep 13 03:20 hello.c
-rw-r--r--. 1 root root 16877 Sep 13 03:22 hello.i
-rw-r--r--. 1 root root 448 Sep 13 03:25 hello.s
[testdir]#
[testdir]# cat hello.s
.file "hello.c"
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)"
.section .note.GNU-stack,"",@progbits
[testdir]#
步骤 3:用 as 汇编

汇编器的目的是将汇编语言指令转换为机器语言代码,并生成扩展名为 .o 的目标文件。此阶段使用默认情况下在所有 Linux 平台上都可用的 GNU 汇编器。

testdir]# as hello.s -o hello.o
[testdir]#
[testdir]# ls -l
total 32
-rw-r--r--. 1 root root 76 Sep 13 03:20 hello.c
-rw-r--r--. 1 root root 16877 Sep 13 03:22 hello.i
-rw-r--r--. 1 root root 1496 Sep 13 03:39 hello.o
-rw-r--r--. 1 root root 448 Sep 13 03:25 hello.s
[testdir]#

现在,你有了第一个 ELF 格式的文件;但是,还不能执行它。稍后,你将看到“目标文件object file”和“可执行文件executable file”之间的区别。

[testdir]# file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
步骤 4:用 ld 链接

这是编译的最后阶段,将目标文件链接以创建可执行文件。可执行文件通常需要外部函数,这些外部函数通常来自系统库(libc)。

你可以使用 ld 命令直接调用链接器;但是,此命令有些复杂。相反,你可以使用带有 -v(详细)标志的 gcc 编译器,以了解链接是如何发生的。(使用 ld 命令进行链接作为一个练习,你可以自行探索。)

[testdir]# gcc -v hello.o
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man [...] --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:[...]:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --eh-frame-hdr --hash-style=gnu [...]/../../../../lib64/crtn.o
[testdir]#

运行此命令后,你应该看到一个名为 a.out 的可执行文件:

[testdir]# ls -l
total 44
-rwxr-xr-x. 1 root root 8440 Sep 13 03:45 a.out
-rw-r--r--. 1 root root 76 Sep 13 03:20 hello.c
-rw-r--r--. 1 root root 16877 Sep 13 03:22 hello.i
-rw-r--r--. 1 root root 1496 Sep 13 03:39 hello.o
-rw-r--r--. 1 root root 448 Sep 13 03:25 hello.s

对 a.out 运行 file 命令,结果表明它确实是 ELF 可执行文件:

[testdir]# file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=48e4c11901d54d4bf1b6e3826baf18215e4255e5, not stripped

运行该可执行文件,看看它是否如源代码所示工作:

[testdir]# ./a.out Hello World

工作了!在幕后发生了很多事情它才在屏幕上打印了 “Hello World”。想象一下在更复杂的程序中会发生什么。

请在第二节查看实例内容“GNU binutils 里的九种武器(—实例)”


via: https://opensource.com/article/19/10/gnu-binutils/

作者:Gaurav Kamathe 选题:lujun9972译者:wxy 校对:wxy

本文由 LCTT 原创翻译,Linux中国 荣誉推出

原文来自:https://linux.cn/article-11441-1.html

本文地址:https://www.linuxprobe.com/gnu-binutils-%e9%87%8c%e7%9a%84%e4%b9%9d%e7%a7%8d%e6%ad%a6%e5%99%a8-%e5%9f%ba%e7%a1%80%e7%9f%a5%e8%af%86.html编辑:张@宇,审核员:逄增宝

Linux命令大全:https://www.linuxcool.com/