标签 GCC 下的文章

无论多么有经验的程序员,开发的任何软件都不可能完全没有 bug。因此,排查及修复 bug 成为软件开发周期中最重要的任务之一。有许多办法可以排查 bug(测试、代码自审等等),但是还有一些专用软件(称为调试器)可以帮助准确定位问题的所在,以便进行修复。

如果你是 C/C++ 程序员,或者使用 Fortran 和 Modula-2 编程语言开发软件,那么你将会很乐意知道有这么一款优秀的调试器 - GDB - 可以帮你更轻松地调试代码 bug 以及其它问题。在这篇文章中,我们将讨论一下 GDB 调试器的基础知识,包括它提供的一些有用的功能/选项。

在我们开始之前,值得一提的是,文章中的所有说明和示例都已经在 Ubuntu 14.04 LTS 中测试过。教程中的示例代码都是 C 语言写的;使用的 shell 为 bash(4.3.11);GDB 版本为 7.7.1。

GDB 调试器基础

通俗的讲,GDB 可以让你看到程序在执行过程时的内部流程,并帮你明确问题的所在。我们将在下一节通过一个有效的示例来讨论 GDB 调试器的用法,但在此之前,我们先来探讨一些之后对你有帮助的基本要点。

首先,为了能够顺利使用类似 GDB 这样的调试器,你必须以指定的方式编译程序,让编译器产生调试器所需的调试信息。例如,在使用 gcc 编译器(我们将在本教程之后的章节用它来编译 C 程序示例)编译代码的时候,你需要使用 -g 命令行选项。

想要了解 gcc 编译器手册页中关于 -g 命令行选项相关的内容,请看这里

下一步,确保在你的系统中已经安装 GDB 调试器。如果没有安装,而且你使用的是基于 Debian 的系统(如 Ubuntu),那么你就可以使用以下命令轻松安装该工具:

sudo apt-get install gdb

在其他发行版上的安装方法,请看这里

现在,当你按照上述的方式编译完程序(gcc -g 命令行选项),同时也已经安装好 GDB 调试器,那么你就可以使用以下命令让程序在调试模式中运行:

gdb [可执行程序的名称]

这样做会初始化 GDB 调试器,但你的可执行程序此时还不会被启动。在这个时候你就可以定义调试相关的设置。例如,你可以在特定行或函数中设置一个断点让 GDB 在该行暂停程序的执行。

接着,为了启动你的程序,你必须输入执行以下 gdb 命令:

run

在这里,值得一提的是,如果你的程序需要一些命令行参数,那么你可以在这里指定这些参数。例如:

run [参数]

GDB 提供了很多有用的命令,在调试的时候总是能派的上用场。我们将在下一节讨论其中一部分命令。

GDB 调试器用例

现在我们对 GDB 及其用法有了基本的概念。因此,让我们举例来应用所学的知识。这是一段示例代码:

#include <stdio.h>

int main()
{
    int out = 0, tot = 0, cnt = 0;
    int val[] = {5, 54, 76, 91, 35, 27, 45, 15, 99, 0};

    while(cnt < 10)
    {
        out = val[cnt];
        tot = tot + 0xffffffff/out;
        cnt++;
    }

    printf("\n Total = [%d]\n", tot);
    return 0;
}

简单说明一下这段代码要做什么事。获取 val 数组中每一个值,将其赋值给 out 变量,然后将 tot 之前的值与 0xffffffff/out 的结果值累加,赋值给 tot 变量。

这里遇到的问题是,当执行这段代码编译后的可执行程序时,产生以下错误:

$ ./gdb-test
Floating point exception (core dumped)

因此,要调试这段代码,第一步是使用 -g 选项编译程序。命令如下:

gcc -g -Wall gdb-test.c -o gdb-test

接着,让我们运行 GDB 调试器并指定要调试的可执行程序。命令如下:

gdb ./gdb-test

现在,我刚才得到的错误是 Floating point exception,大部分人可能已经知道,这是因为 n % x,当 x 为 0 时导致的错误。所以,考虑到这一点,我在 11 行代码除法运算的位置处添加了一个断点。如下:

(gdb)&;break 11

注意 (gdb) 是调试器的提示信息,我只输入了 break 11 命令。

现在,让 GDB 开始运行程序:

run

当断点第一次被命中时,GDB 显示如下输出:

Breakpoint 1, main () at gdb-test.c:11
11 tot = tot + 0xffffffff/out;
(gdb)

正如你所看到的那样,调试器会显示断点所在的行代码。现在,让我们打印出此时 out 的值。如下:

(gdb) print out
$1 = 5
(gdb)

如上所示,值 5 被打印出来了。这个时候一切都还是正常的。让调试器继续执行程序直到命中下一个断点,可以通过使用 c 命令来完成:

c

重复上述操作,直到 out 值变为 0 时。

...
...
...
Breakpoint 1, main () at gdb-test.c:11
11 tot = tot + 0xffffffff/out;
(gdb) print out
$2 = 99
(gdb) c
Continuing.

Breakpoint 1, main () at gdb-test.c:11
11 tot = tot + 0xffffffff/out;
(gdb) print out
$3 = 0
(gdb)

现在,为了进一步确认问题,我使用 GDB 的 s(或 step) 命令代替 c 命令。因为,我只想让当前程序在第 11 行之后暂停,再一步步执行,看看这个时候是否会发生崩溃。

以下是执行之后输出信息:

(gdb) s

Program received signal SIGFPE, Arithmetic exception.
0x080484aa in main () at gdb-test.c:11
11 tot = tot + 0xffffffff/out;

是的,如上输出的第一行内容所示,这就是抛出异常的地方。当我再次尝试运行 s 命令时,问题最终也得到了确认:

(gdb) s

Program terminated with signal SIGFPE, Arithmetic exception.
The program no longer exists.

通过这种方式,你就可以使用 GDB 调试你的程序。

总结

GDB 提供了很多功能供用户研究和使用,在这里,我们仅仅只介绍了很少一部分内容。通过 GDB 的手册页可以进一步了解这个工具,当你在调试代码的时候,尝试使用一下它。GDB 调试器有一定的学习难度,但是它很值得你下功夫学习。


via: https://www.howtoforge.com/tutorial/how-to-debug-c-programs-in-linux-using-gdb/

作者:Ansh 译者:zhb127 校对:jasminepeng

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

gcc 编译器提供了几乎数不清的命令行选项列表。当然,没有人会使用过或者精通它所有的命令行选项,但是有一些命令行选项是每一个 gcc 用户都应该知道的 - 即使不是必须知道。它们中有一些很常用,其他一些不太常用,但不常用并不意味着它们的用处没前者大。

在这个系列的文章中,我们集中于一些不常用但是很有用的 gcc 命令行选项,在第一节已经讲到几个这样的命令行选项。

不知道你是否能够回想起,在这个系列教程的第一部分的开始,我简要的提到了开发者们通常用来生成警告的 -Wall 选项,并不包括一些特殊的警告。如果你不了解这些特殊警告,并且不知道如何生成它们,不用担心,我将在这篇文章中详细讲解关于它们所有的细节。

除此以外,这篇文章也将涉及与浮点值相关的 gcc 警告选项,以及在 gcc 命令行选项列表变得很大的时候如何更好的管理它们。

在继续之前,请记住,这个教程中的所有例子、命令和指令都已在 Ubuntu 16.04 LTS 操作系统和 gcc 5.4.0 上测试过。

生成 -Wall 选项不包括的警告

尽管 gcc 编译器的 -Wall 选项涵盖了绝大多数警告标记,依然有一些警告不能生成。为了生成它们,请使用 -Wextra 选项。

比如,下面的代码:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int i=0;
    /* ...
       some code here 
       ...
    */

    if(i);
        return 1;
     return 0; 
}

我不小心在 if 条件后面多打了一个分号。现在,如果使用下面的 gcc 命令来进行编译,不会生成任何警告。

gcc -Wall test.c -o test

但是如果同时使用 -Wextra 选项来进行编译:

gcc -Wall -Wextra test.c -o test

会生成下面这样一个警告:

test.c: In function ‘main’:
test.c:10:8: warning: suggest braces around empty body in an ‘if’ statement [-Wempty-body]
 if(i);

从上面的警告清楚的看到, -Wextra 选项从内部启用了 -Wempty-body 选项,从而可以检测可疑代码并生成警告。下面是这个选项启用的全部警告标记。

  • -Wclobbered
  • -Wempty-body
  • -Wignored-qualifiers
  • -Wmissing-field-initializers
  • -Wmissing-parameter-type (仅针对 C 语言)
  • -Wold-style-declaration (仅针对 C 语言)
  • -Woverride-init
  • -Wsign-compare
  • -Wtype-limits
  • -Wuninitialized
  • -Wunused-parameter (只有和 -Wunused-Wall 选项使用时才会启用)
  • -Wunused-but-set-parameter (只有和-Wunused-Wall` 选项使用时才会生成)

如果想对上面所提到的标记有更进一步的了解,请查看 gcc 手册

此外,遇到下面这些情况, -Wextra 选项也会生成警告:

  • 一个指针和整数 0 进行 <<=>, 或 >= 比较
  • (仅 C++)一个枚举类型和一个非枚举类型同时出现在一个条件表达式中
  • (仅 C++)有歧义的虚拟基底
  • (仅 C++)寄存器类型的数组加下标
  • (仅 C++)对寄存器类型的变量进行取址
  • (仅 C++)基类没有在派生类的复制构建函数中进行初始化

浮点值的等值比较时生成警告

你可能已经知道,浮点值不能进行确切的相等比较(如果不知道,请阅读与浮点值比较相关的 FAQ)。但是如果你不小心这样做了, gcc 编译器是否会报出错误或警告?让我们来测试一下:

下面是一段使用 == 运算符进行浮点值比较的代码:

#include<stdio.h>

void compare(float x, float y)
{
    if(x == y)
    {
        printf("\n EQUAL \n");
    }
}

int main(void)
{
    compare(1.234, 1.56789);

    return 0; 
}

使用下面的 gcc 命令(包含 -Wall-Wextra 选项)来编译这段代码:

gcc -Wall -Wextra test.c -o test

遗憾的是,上面的命令没有生成任何与浮点值比较相关的警告。快速看一下 gcc 手册,在这种情形下可以使用一个专用的 -Wfloat-equal 选项。

下面是包含这个选项的命令:

gcc -Wall -Wextra -Wfloat-equal test.c -o test

下面是这条命令产生的输出:

test.c: In function ‘compare’:
test.c:5:10: warning: comparing floating point with == or != is unsafe [-Wfloat-equal]
 if(x == y)

正如上面你所看到的输出那样, -Wfloat-equal 选项会强制 gcc 编译器生成一个与浮点值比较相关的警告。

这儿是gcc 手册关于这一选项的说明:

这背后的想法是,有时,对程序员来说,把浮点值考虑成近似无限精确的实数是方便的。如果你这样做,那么你需要通过分析代码,或者其他方式,算出这种计算方式引入的最大或可能的最大误差,然后进行比较时(以及产生输出时,不过这是一个不同的问题)允许这个误差。特别要指出,不应该检查是否相等,而应该检查两个值是否可能出现范围重叠;这是用关系运算符来做的,所以等值比较可能是搞错了。

如何更好的管理 gcc 命令行选项

如果在你使用的 gcc 命令中,命令行选项列表变得很大而且很难管理,那么你可以把它放在一个文本文件中,然后把文件名作为 gcc 命令的一个参数。之后,你必须使用 @file 命令行选项。

比如,下面这行是你的 gcc 命令:

gcc -Wall -Wextra -Wfloat-equal test.c -o test

然后你可以把这三个和警告相关的选项放到一个文件里,文件名叫做 gcc-options

$ cat gcc-options&nbsp;
-Wall -Wextra -Wfloat-equal

这样,你的 gcc 命令会变得更加简洁并且易于管理:

gcc @gcc-options test.c -o test

下面是 gcc 手册关于 @file 的说明:

从文件中读取命令行选项。读取到的选项随之被插入到原始 @file 选项所在的位置。如果文件不存在或者无法读取,那么这个选项就会被当成文字处理,而不会被删除。

文件中的选项以空格分隔。选项中包含空白字符的话,可以用一个由单引号或双引号包围完整选项。任何字符(包括反斜杠: '\')均可能通过一个 '\' 前缀而包含在一个选项中。如果该文件本身包含额外的 @file 选项,那么它将会被递归处理。

结论

在这个系列的教程中,我们一共讲解了 5 个不常见但是很有用的 gcc 命令行选项: -Save-temps-g-Wextra-Wfloat-equal 以及 @file。记得花时间练习使用每一个选项,同时不要忘了浏览 gcc 手册上面所提供的关于它们的全部细节。

你是否知道或使用其他像这样有用的 gcc 命令行选项,并希望把它们在全世界范围内分享?请在下面的评论区留下所有的细节。


via: https://www.howtoforge.com/tutorial/uncommon-but-useful-gcc-command-line-options-2/

作者:Ansh 译者:ucasFL 校对:jasminepeng

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

软件工具通常情况下会提供多个功能以供选择,但是如你所知的,不是所有的功能都能被每个人用到的。公正地讲,这并不是设计上的错误,因为每个用户都会有自己的需求,他们只在他们的领域内使用该工具。然而,深入了解你所使用的工具也是很有益处的,因为你永远不知道它的某个功能会在什么时候派上用场,从而节省下你宝贵的时间。

举一个例子:编译器。一个优秀的编程语言编译器总是会提供极多的选项,但是用户一般只知道和使用其中很有限的一部分功能。更具体点来说,比如你是 C 语言开发人员,并将 Linux 作为你的开发平台,那么你很有可能会用到 gcc 编译器,这个编译器提供了 (几乎) 数不清的命令行选项列表。

你知道,你可以让 gcc 保存每个编译阶段的输出吗?你知道用于生成警告的 -Wall 选项,它并不会包含一些特殊的警告吗?gcc 的很多命令行选项都不会经常用到,但是它们在某些特定的情况下会变得非常有用,例如,当你在调试代码的时候。

所以在本文中,我们会介绍这样的几个选项,提供所有必要的细节,并通过简单易懂的例子来解释它们。

但是在开始前,请注意本文中所有的例子所使用的环境:基于 Ubuntu 16.04 LTS 操作系统,gcc 版本为 5.4.0。

在每个编译阶段查看中间代码的输出

你知道在通过 gcc 编译 c 语言代码的时候大体上共分为四个阶段吗?分别为预处理 -> 编译 -> 汇编 -> 链接。在每个阶段之后,gcc 都会产生一个将移交给下一个阶段的临时输出文件。但是生成的都是临时文件,因此我们并不能看到它们——我们所看到的只是我们发起编译命令,然后它生成的我们可以直接运行的二进制文件或可执行文件。

但是比如说在预处理阶段,如果调试时需要查看代码是如何进行处理的,你要怎么做呢?好消息是 gcc 编译器提供了相应的命令行选项,你可以在标准编译命令中使用这些选项获得原本被编译器删除的中间文件。我们所说的选项就是-save-temps

以下是 gcc 手册中对该选项的介绍:

永久存储临时的中间文件,将它们放在当前的文件夹下并根据源文件名称为其命名。因此,用 -c -save-temps 命令编译 foo.c 文件时会生成 foo.i foo.s 和 foo.o 文件。即使现在编译器大多使用的是集成的预处理器,这命令也会生成预处理输出文件 foo.i。

当与 -x 命令行选项结合使用时,-save-temps 命令会避免覆写与中间文件有着相同扩展名的输入源文件。相应的中间文件可以通过在使用 -save-temps 命令之前重命名源文件获得。

以下是怎样使用这个选项的例子:

gcc -Wall -save-temps test.c -o test-exec

下图为该命令的执行结果,验证其确实产生了中间文件:

因此,在截图中你所看到的 test.i、test.s、 test.o 文件都是由 -save-temps 选项产生的。这些文件分别对应于预处理、编译和链接阶段。

让你的代码可调试和可分析

你可以使用专有的工具调试和分析代码。如 gdb 就是专用于调试的工具,而 gprof 则是热门的分析工具。但你知道 gcc 特定的命令行选项也可以让你的代码可调试和可分析吗?

让我们开始调试之路吧!为了能在代码调试中使用 gdb,你需要在编译代码的时候使用 gcc 编译器提供的 -g 选项。这个选项让 gcc 生成 gdb 需要的调试信息从而能成功地调试程序。

如果你想要使用此选项,建议您详细阅读 gcc 手册提供的有关此选项的详细信息——在某些情况下,其中的一些内容可能是至关重要的。 例如,以下是从手册页中摘录的内容:

GCC 允许在使用 -g 选项的时候配合使用 -O 选项。优化代码采用的便捷方式有时可能会产生意想不到的结果:某些你声明的变量可能不复存在;控制流可能会突然跳转到你未曾预期的位置;一些语句也许不会执行,因为它们已经把常量结果计算了或值已经被保存;一些语句可能会在不同地方执行,因为它们已经被移出循环。

然而优化的输出也是可以调试的。这就使得让优化器可以合理地优化或许有 bug 的代码。

不只是 gdb,使用 -g 选项编译代码,还可以开启使用 Valgrind 内存检测工具,从而完全发挥出该选项的潜力。或许还有一些人不知道,mencheck 工具被程序员们用来检测代码中是否存在内存泄露。你可以在这里参见这个工具的用法。

继续往下,为了能够在代码分析中使用 gprof 工具,你需要使用 -pg 命令行选项来编译代码。这会让 gcc 生成额外的代码来写入分析信息,gprof 工具需要这些信息来进行代码分析。gcc 手册 中提到:当编译你需要数据的源文件时,你必须使用这个选项,当然链接时也需要使用它。为了能了解 gprof 分析代码时具体是如何工作的,你可以转到我们的网站专用教程进行了解。

注意-g-pg 选项的用法类似于上一节中使用 -save-temps 选项的方式。

结论

我相信除了 gcc 的专业人士,都可以在这篇文章中得到了一些启发。尝试一下这些选项,然后观察它们是如何工作的。同时,请期待本教程系列的下一部分,我们将会讨论更多有趣和有用的 gcc 命令行选项。


via: https://www.howtoforge.com/tutorial/uncommon-but-useful-gcc-command-line-options/

作者:Ansh 译者:dongdongmian 校对:wxy

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

v0.1, 01 March 2003.

本 HOWTO 文档将讲解 GCC 提供的内联汇编特性的用途和用法。对于阅读这篇文章,这里只有两个前提要求,很明显,就是 x86 汇编语言和 C 语言的基本认识。

1. 简介

1.1 版权许可

Copyright (C) 2003 Sandeep S.

本文档自由共享;你可以重新发布它,并且/或者在遵循自由软件基金会发布的 GNU 通用公共许可证下修改它;也可以是该许可证的版本 2 或者(按照你的需求)更晚的版本。

发布这篇文档是希望它能够帮助别人,但是没有任何担保;甚至不包括可售性和适用于任何特定目的的担保。关于更详细的信息,可以查看 GNU 通用许可证。

1.2 反馈校正

请将反馈和批评一起提交给 Sandeep.S 。我将感谢任何一个指出本文档中错误和不准确之处的人;一被告知,我会马上改正它们。

1.3 致谢

我对提供如此棒的特性的 GNU 人们表示真诚的感谢。感谢 Mr.Pramode C E 所做的所有帮助。感谢在 Govt Engineering College 和 Trichur 的朋友们的精神支持和合作,尤其是 Nisha Kurur 和 Sakeeb S 。 感谢在 Gvot Engineering College 和 Trichur 的老师们的合作。

另外,感谢 Phillip , Brennan Underwood 和 [email protected] ;这里的许多东西都厚颜地直接取自他们的工作成果。

2. 概览

在这里,我们将学习 GCC 内联汇编。这里 内联 inline 表示的是什么呢?

我们可以要求编译器将一个函数的代码插入到调用者代码中函数被实际调用的地方。这样的函数就是内联函数。这听起来和宏差不多?这两者确实有相似之处。

内联函数的优点是什么呢?

这种内联方法可以减少函数调用开销。同时如果所有实参的值为常量,它们的已知值可以在编译期允许简化,因此并非所有的内联函数代码都需要被包含进去。代码大小的影响是不可预测的,这取决于特定的情况。为了声明一个内联函数,我们必须在函数声明中使用 inline 关键字。

现在我们正处于一个猜测内联汇编到底是什么的点上。它只不过是一些写为内联函数的汇编程序。在系统编程上,它们方便、快速并且极其有用。我们主要集中学习(GCC)内联汇编函数的基本格式和用法。为了声明内联汇编函数,我们使用 asm 关键词。

内联汇编之所以重要,主要是因为它可以操作并且使其输出通过 C 变量显示出来。正是因为此能力, "asm" 可以用作汇编指令和包含它的 C 程序之间的接口。

3. GCC 汇编语法

Linux上的 GNU C 编译器 GCC ,使用 AT&T / UNIX 汇编语法。在这里,我们将使用 AT&T 语法 进行汇编编码。如果你对 AT&T 语法不熟悉的话,请不要紧张,我会教你的。AT&T 语法和 Intel 语法的差别很大。我会给出主要的区别。

  1. 源操作数和目的操作数顺序

AT&T 语法的操作数方向和 Intel 语法的刚好相反。在Intel 语法中,第一操作数为目的操作数,第二操作数为源操作数,然而在 AT&T 语法中,第一操作数为源操作数,第二操作数为目的操作数。也就是说,

Intel 语法中的 Op-code dst src 变为 AT&T 语法中的 Op-code src dst

  1. 寄存器命名

寄存器名称有 % 前缀,即如果必须使用 eax,它应该用作 %eax

  1. 立即数

AT&T 立即数以 $ 为前缀。静态 "C" 变量也使用 $ 前缀。在 Intel 语法中,十六进制常量以 h 为后缀,然而 AT&T 不使用这种语法,这里我们给常量添加前缀 0x。所以,对于十六进制,我们首先看到一个 $,然后是 0x,最后才是常量。

  1. 操作数大小

在 AT&T 语法中,存储器操作数的大小取决于操作码名字的最后一个字符。操作码后缀 ’b’ 、’w’、’l’ 分别指明了 字节 byte (8位)、 word (16位)、 长型 long (32位)存储器引用。Intel 语法通过给存储器操作数添加 byte ptrword ptrdword ptr 前缀来实现这一功能。

因此,Intel的 mov al, byte ptr foo 在 AT&T 语法中为 movb foo, %al

  1. 存储器操作数

在 Intel 语法中,基址寄存器包含在 [] 中,然而在 AT&T 中,它们变为 ()。另外,在 Intel 语法中, 间接内存引用为

section:[base + index*scale + disp],在 AT&T中变为 section:disp(base, index, scale)

需要牢记的一点是,当一个常量用于 disp 或 scale,不能添加 $ 前缀。

现在我们看到了 Intel 语法和 AT&T 语法之间的一些主要差别。我仅仅写了它们差别的一部分而已。关于更完整的信息,请参考 GNU 汇编文档。现在为了更好地理解,我们可以看一些示例。

+------------------------------+------------------------------------+
|       Intel Code             |      AT&T Code                     |
+------------------------------+------------------------------------+
| mov     eax,1                |  movl    $1,%eax                   |   
| mov     ebx,0ffh             |  movl    $0xff,%ebx                |   
| int     80h                  |  int     $0x80                     |   
| mov     ebx, eax             |  movl    %eax, %ebx                |
| mov     eax,[ecx]            |  movl    (%ecx),%eax               |
| mov     eax,[ebx+3]          |  movl    3(%ebx),%eax              | 
| mov     eax,[ebx+20h]        |  movl    0x20(%ebx),%eax           |
| add     eax,[ebx+ecx*2h]     |  addl    (%ebx,%ecx,0x2),%eax      |
| lea     eax,[ebx+ecx]        |  leal    (%ebx,%ecx),%eax          |
| sub     eax,[ebx+ecx*4h-20h] |  subl    -0x20(%ebx,%ecx,0x4),%eax |
+------------------------------+------------------------------------+

4. 基本内联

基本内联汇编的格式非常直接了当。它的基本格式为

asm("汇编代码");

示例

asm("movl %ecx %eax"); /* 将 ecx 寄存器的内容移至 eax  */
__asm__("movb %bh (%eax)"); /* 将 bh 的一个字节数据 移至 eax 寄存器指向的内存 */

你可能注意到了这里我使用了 asm__asm__。这两者都是有效的。如果关键词 asm 和我们程序的一些标识符冲突了,我们可以使用 __asm__。如果我们的指令多于一条,我们可以每个一行,并用双引号圈起,同时为每条指令添加 ’\n’ 和 ’\t’ 后缀。这是因为 gcc 将每一条当作字符串发送给 as(GAS)(LCTT 译注: GAS 即 GNU 汇编器),并且通过使用换行符/制表符发送正确格式化后的行给汇编器。

示例

__asm__ ("movl %eax, %ebx\n\t"
         "movl $56, %esi\n\t"
         "movl %ecx, $label(%edx,%ebx,$4)\n\t"
         "movb %ah, (%ebx)");

如果在代码中,我们涉及到一些寄存器(即改变其内容),但在没有恢复这些变化的情况下从汇编中返回,这将会导致一些意想不到的事情。这是因为 GCC 并不知道寄存器内容的变化,这会导致问题,特别是当编译器做了某些优化。在没有告知 GCC 的情况下,它将会假设一些寄存器存储了一些值——而我们可能已经改变却没有告知 GCC——它会像什么事都没发生一样继续运行(LCTT 译注:什么事都没发生一样是指GCC不会假设寄存器装入的值是有效的,当退出改变了寄存器值的内联汇编后,寄存器的值不会保存到相应的变量或内存空间)。我们所可以做的是使用那些没有副作用的指令,或者当我们退出时恢复这些寄存器,要不就等着程序崩溃吧。这是为什么我们需要一些扩展功能,扩展汇编给我们提供了那些功能。

5. 扩展汇编

在基本内联汇编中,我们只有指令。然而在扩展汇编中,我们可以同时指定操作数。它允许我们指定输入寄存器、输出寄存器以及修饰寄存器列表。GCC 不强制用户必须指定使用的寄存器。我们可以把头疼的事留给 GCC ,这可能可以更好地适应 GCC 的优化。不管怎么说,基本格式为:

asm ( 汇编程序模板 
    : 输出操作数                   /* 可选的 */
    : 输入操作数                  /* 可选的 */
    : 修饰寄存器列表             /* 可选的 */
    );

汇编程序模板由汇编指令组成。每一个操作数由一个操作数约束字符串所描述,其后紧接一个括弧括起的 C 表达式。冒号用于将汇编程序模板和第一个输出操作数分开,另一个(冒号)用于将最后一个输出操作数和第一个输入操作数分开(如果存在的话)。逗号用于分离每一个组内的操作数。总操作数的数目限制在 10 个,或者机器描述中的任何指令格式中的最大操作数数目,以较大者为准。

如果没有输出操作数但存在输入操作数,你必须将两个连续的冒号放置于输出操作数原本会放置的地方周围。

示例:

asm ("cld\n\t"
     "rep\n\t"
     "stosl"
     : /* 无输出寄存器 */
     : "c" (count), "a" (fill_value), "D" (dest)
     : "%ecx", "%edi" 
     );

现在来看看这段代码是干什么的?以上的内联汇编是将 fill_value 值连续 count 次拷贝到寄存器 edi 所指位置(LCTT 译注:每执行 stosl 一次,寄存器 edi 的值会递增或递减,这取决于是否设置了 direction 标志,因此以上代码实则初始化一个内存块)。 它也告诉 gcc 寄存器 ecxedi 一直无效(LCTT 译注:原文为 eax ,但代码修饰寄存器列表中为 ecx,因此这可能为作者的纰漏。)。为了更加清晰地说明,让我们再看一个示例。

int a=10, b;
asm ("movl %1, %%eax; 
      movl %%eax, %0;"
     :"=r"(b)        /* 输出 */
     :"r"(a)         /* 输入 */
     :"%eax"         /* 修饰寄存器 */
     );       

这里我们所做的是使用汇编指令使 ’b’ 变量的值等于 ’a’ 变量的值。一些有意思的地方是:

  • "b" 为输出操作数,用 %0 引用,并且 "a" 为输入操作数,用 %1 引用。
  • "r" 为操作数约束。之后我们会更详细地了解约束(字符串)。目前,"r" 告诉 GCC 可以使用任一寄存器存储操作数。输出操作数约束应该有一个约束修饰符 "=" 。这修饰符表明它是一个只读的输出操作数。
  • 寄存器名字以两个 % 为前缀。这有利于 GCC 区分操作数和寄存器。操作数以一个 % 为前缀。
  • 第三个冒号之后的修饰寄存器 %eax 用于告诉 GCC %eax 的值将会在 "asm" 内部被修改,所以 GCC 将不会使用此寄存器存储任何其他值。

当 “asm” 执行完毕, "b" 变量会映射到更新的值,因为它被指定为输出操作数。换句话说, “asm” 内 "b" 变量的修改应该会被映射到 “asm” 外部。

现在,我们可以更详细地看看每一个域。

5.1 汇编程序模板

汇编程序模板包含了被插入到 C 程序的汇编指令集。其格式为:每条指令用双引号圈起,或者整个指令组用双引号圈起。同时每条指令应以分界符结尾。有效的分界符有换行符(\n)和分号(;)。\n 可以紧随一个制表符(\t)。我们应该都明白使用换行符或制表符的原因了吧(LCTT 译注:就是为了排版和分隔)?和 C 表达式对应的操作数使用 %0、%1 ... 等等表示。

5.2 操作数

C 表达式用作 “asm” 内的汇编指令操作数。每个操作数前面是以双引号圈起的操作数约束。对于输出操作数,在引号内还有一个约束修饰符,其后紧随一个用于表示操作数的 C 表达式。即,“操作数约束”(C 表达式)是一个通用格式。对于输出操作数,还有一个额外的修饰符。约束字符串主要用于决定操作数的寻址方式,同时也用于指定使用的寄存器。

如果我们使用的操作数多于一个,那么每一个操作数用逗号隔开。

在汇编程序模板中,每个操作数用数字引用。编号方式如下。如果总共有 n 个操作数(包括输入和输出操作数),那么第一个输出操作数编号为 0 ,逐项递增,并且最后一个输入操作数编号为 n - 1 。操作数的最大数目在前一节我们讲过。

输出操作数表达式必须为左值。输入操作数的要求不像这样严格。它们可以为表达式。扩展汇编特性常常用于编译器所不知道的机器指令 ;-)。如果输出表达式无法直接寻址(即,它是一个位域),我们的约束字符串必须给定一个寄存器。在这种情况下,GCC 将会使用该寄存器作为汇编的输出,然后存储该寄存器的内容到输出。

正如前面所陈述的一样,普通的输出操作数必须为只写的; GCC 将会假设指令前的操作数值是死的,并且不需要被(提前)生成。扩展汇编也支持输入-输出或者读-写操作数。

所以现在我们来关注一些示例。我们想要求一个数的5次方结果。为了计算该值,我们使用 lea 指令。

asm ("leal (%1,%1,4), %0"
     : "=r" (five_times_x)
     : "r" (x) 
     );

这里我们的输入为 x。我们不指定使用的寄存器。 GCC 将会选择一些输入寄存器,一个输出寄存器,来做我们预期的工作。如果我们想要输入和输出放在同一个寄存器里,我们也可以要求 GCC 这样做。这里我们使用那些读-写操作数类型。这里我们通过指定合适的约束来实现它。

asm ("leal (%0,%0,4), %0"
     : "=r" (five_times_x)
     : "0" (x) 
     );

现在输出和输出操作数位于同一个寄存器。但是我们无法得知是哪一个寄存器。现在假如我们也想要指定操作数所在的寄存器,这里有一种方法。

asm ("leal (%%ecx,%%ecx,4), %%ecx"
     : "=c" (x)
     : "c" (x) 
     );

在以上三个示例中,我们并没有在修饰寄存器列表里添加任何寄存器,为什么?在头两个示例, GCC 决定了寄存器并且它知道发生了什么改变。在最后一个示例,我们不必将 'ecx' 添加到修饰寄存器列表(LCTT 译注: 原文修饰寄存器列表这个单词拼写有错,这里已修正),gcc 知道它表示 x。因此,因为它可以知道 ecx 的值,它就不被当作修饰的(寄存器)了。

5.3 修饰寄存器列表

一些指令会破坏一些硬件寄存器内容。我们不得不在修饰寄存器中列出这些寄存器,即汇编函数内第三个 ’:’ 之后的域。这可以通知 gcc 我们将会自己使用和修改这些寄存器,这样 gcc 就不会假设存入这些寄存器的值是有效的。我们不用在这个列表里列出输入、输出寄存器。因为 gcc 知道 “asm” 使用了它们(因为它们被显式地指定为约束了)。如果指令隐式或显式地使用了任何其他寄存器,(并且寄存器没有出现在输出或者输出约束列表里),那么就需要在修饰寄存器列表中指定这些寄存器。

如果我们的指令可以修改条件码寄存器(cc),我们必须将 "cc" 添加进修饰寄存器列表。

如果我们的指令以不可预测的方式修改了内存,那么需要将 "memory" 添加进修饰寄存器列表。这可以使 GCC 不会在汇编指令间保持缓存于寄存器的内存值。如果被影响的内存不在汇编的输入或输出列表中,我们也必须添加 volatile 关键词。

我们可以按我们的需求多次读写修饰寄存器。参考一下模板内的多指令示例;它假设子例程 \_foo 接受寄存器 eaxecx 里的参数。

asm ("movl %0,%%eax;
      movl %1,%%ecx;
      call _foo"
     : /* no outputs */
     : "g" (from), "g" (to)
     : "eax", "ecx"
     );

5.4 Volatile ...?

如果你熟悉内核源码或者类似漂亮的代码,你一定见过许多声明为 volatile 或者 __volatile__的函数,其跟着一个 asm 或者 __asm__。我之前提到过关键词 asm__asm__。那么什么是 volatile 呢?

如果我们的汇编语句必须在我们放置它的地方执行(例如,不能为了优化而被移出循环语句),将关键词 volatile 放置在 asm 后面、()的前面。以防止它被移动、删除或者其他操作,我们将其声明为 asm volatile ( ... : ... : ... : ...);

如果担心发生冲突,请使用 __volatile__

如果我们的汇编只是用于一些计算并且没有任何副作用,不使用 volatile 关键词会更好。不使用 volatile 可以帮助 gcc 优化代码并使代码更漂亮。

在“一些实用的诀窍”一节中,我提供了多个内联汇编函数的例子。那里我们可以了解到修饰寄存器列表的细节。

6. 更多关于约束

到这个时候,你可能已经了解到约束和内联汇编有很大的关联。但我们对约束讲的还不多。约束用于表明一个操作数是否可以位于寄存器和位于哪种寄存器;操作数是否可以为一个内存引用和哪种地址;操作数是否可以为一个立即数和它可能的取值范围(即值的范围),等等。

6.1 常用约束

在许多约束中,只有小部分是常用的。我们来看看这些约束。

  1. 寄存器操作数约束(r)

当使用这种约束指定操作数时,它们存储在通用寄存器(GPR)中。请看下面示例:

asm ("movl %%eax, %0\n" :"=r"(myval));

这里,变量 myval 保存在寄存器中,寄存器 eax 的值被复制到该寄存器中,并且 myval 的值从寄存器更新到了内存。当指定 "r" 约束时, gcc 可以将变量保存在任何可用的 GPR 中。要指定寄存器,你必须使用特定寄存器约束直接地指定寄存器的名字。它们为:

+---+--------------------+
| r |    Register(s)     |
+---+--------------------+
| a |   %eax, %ax, %al   |
| b |   %ebx, %bx, %bl   |
| c |   %ecx, %cx, %cl   |
| d |   %edx, %dx, %dl   |
| S |   %esi, %si        |
| D |   %edi, %di        |
+---+--------------------+
  1. 内存操作数约束(m)

当操作数位于内存时,任何对它们的操作将直接发生在内存位置,这与寄存器约束相反,后者首先将值存储在要修改的寄存器中,然后将它写回到内存位置。但寄存器约束通常用于一个指令必须使用它们或者它们可以大大提高处理速度的地方。当需要在 “asm” 内更新一个 C 变量,而又不想使用寄存器去保存它的值,使用内存最为有效。例如,IDTR 寄存器的值存储于内存位置 loc 处:

asm("sidt %0\n" : :"m"(loc));

  1. 匹配(数字)约束

在某些情况下,一个变量可能既充当输入操作数,也充当输出操作数。可以通过使用匹配约束在 "asm" 中指定这种情况。

asm ("incl %0" :"=a"(var):"0"(var));

在操作数那一节中,我们也看到了一些类似的示例。在这个匹配约束的示例中,寄存器 "%eax" 既用作输入变量,也用作输出变量。 var 输入被读进 %eax,并且等递增后更新的 %eax 再次被存储进 var。这里的 "0" 用于指定与第 0 个输出变量相同的约束。也就是,它指定 var 输出实例应只被存储在 "%eax" 中。该约束可用于:

* 在输入从变量读取或变量修改后且修改被写回同一变量的情况
* 在不需要将输入操作数实例和输出操作数实例分开的情况使用匹配约束最重要的意义在于它们可以有效地使用可用寄存器。

其他一些约束:

  1. "m" : 允许一个内存操作数,可以使用机器普遍支持的任一种地址。
  2. "o" : 允许一个内存操作数,但只有当地址是可偏移的。即,该地址加上一个小的偏移量可以得到一个有效地址。
  3. "V" : 一个不允许偏移的内存操作数。换言之,任何适合 "m" 约束而不适合 "o" 约束的操作数。
  4. "i" : 允许一个(带有常量)的立即整形操作数。这包括其值仅在汇编时期知道的符号常量。
  5. "n" : 允许一个带有已知数字的立即整形操作数。许多系统不支持汇编时期的常量,因为操作数少于一个字宽。对于此种操作数,约束应该使用 'n' 而不是'i'。
  6. "g" : 允许任一寄存器、内存或者立即整形操作数,不包括通用寄存器之外的寄存器。

以下约束为 x86 特有。

  1. "r" : 寄存器操作数约束,查看上面给定的表格。
  2. "q" : 寄存器 a、b、c 或者 d。
  3. "I" : 范围从 0 到 31 的常量(对于 32 位移位)。
  4. "J" : 范围从 0 到 63 的常量(对于 64 位移位)。
  5. "K" : 0xff。
  6. "L" : 0xffff。
  7. "M" : 0、1、2 或 3 (lea 指令的移位)。
  8. "N" : 范围从 0 到 255 的常量(对于 out 指令)。
  9. "f" : 浮点寄存器
  10. "t" : 第一个(栈顶)浮点寄存器
  11. "u" : 第二个浮点寄存器
  12. "A" : 指定 ad 寄存器。这主要用于想要返回 64 位整形数,使用 d 寄存器保存最高有效位和 a 寄存器保存最低有效位。

6.2 约束修饰符

当使用约束时,对于更精确的控制超过了对约束作用的需求,GCC 给我们提供了约束修饰符。最常用的约束修饰符为:

  1. "=" : 意味着对于这条指令,操作数为只写的;旧值会被忽略并被输出数据所替换。
  2. "&" : 意味着这个操作数为一个早期改动的操作数,其在该指令完成前通过使用输入操作数被修改了。因此,这个操作数不可以位于一个被用作输出操作数或任何内存地址部分的寄存器。如果在旧值被写入之前它仅用作输入而已,一个输入操作数可以为一个早期改动操作数。

上述的约束列表和解释并不完整。示例可以让我们对内联汇编的用途和用法更好的理解。在下一节,我们会看到一些示例,在那里我们会发现更多关于修饰寄存器列表的东西。

7. 一些实用的诀窍

现在我们已经介绍了关于 GCC 内联汇编的基础理论,现在我们将专注于一些简单的例子。将内联汇编函数写成宏的形式总是非常方便的。我们可以在 Linux 内核代码里看到许多汇编函数。(usr/src/linux/include/asm/*.h)。

  1. 首先我们从一个简单的例子入手。我们将写一个两个数相加的程序。
int main(void)
{
        int foo = 10, bar = 15;
        __asm__ __volatile__("addl  %%ebx,%%eax"
                             :"=a"(foo)
                             :"a"(foo), "b"(bar)
                             );
        printf("foo+bar=%d\n", foo);
        return 0;
}

这里我们要求 GCC 将 foo 存放于 %eax,将 bar 存放于 %ebx,同时我们也想要在 %eax 中存放结果。'=' 符号表示它是一个输出寄存器。现在我们可以以其他方式将一个整数加到一个变量。

__asm__ __volatile__(
                     "   lock       ;\n"
                     "   addl %1,%0 ;\n"
                     : "=m"  (my_var)
                     : "ir"  (my_int), "m" (my_var)
                     :                                 /* 无修饰寄存器列表 */
                     );

这是一个原子加法。为了移除原子性,我们可以移除指令 'lock'。在输出域中,"=m" 表明 myvar 是一个输出且位于内存。类似地,"ir" 表明 myint 是一个整型,并应该存在于其他寄存器(回想我们上面看到的表格)。没有寄存器位于修饰寄存器列表中。

  1. 现在我们将在一些寄存器/变量上展示一些操作,并比较值。
__asm__ __volatile__(  "decl %0; sete %1"
                     : "=m" (my_var), "=q" (cond)
                     : "m" (my_var) 
                     : "memory"
                     );

这里,my\_var 的值减 1 ,并且如果结果的值为 0,则变量 cond 置 1。我们可以通过将指令 "lock;\n\t" 添加为汇编模板的第一条指令以增加原子性。

以类似的方式,为了增加 my\_var,我们可以使用 "incl %0" 而不是 "decl %0"。

这里需要注意的地方是(i)my\_var 是一个存储于内存的变量。(ii)cond 位于寄存器 eax、ebx、ecx、edx 中的任何一个。约束 "=q" 保证了这一点。(iii)同时我们可以看到 memory 位于修饰寄存器列表中。也就是说,代码将改变内存中的内容。

  1. 如何置 1 或清 0 寄存器中的一个比特位。作为下一个诀窍,我们将会看到它。
__asm__ __volatile__(   "btsl %1,%0"
                      : "=m" (ADDR)
                      : "Ir" (pos)
                      : "cc"
                      );

这里,ADDR 变量(一个内存变量)的 'pos' 位置上的比特被设置为 1。我们可以使用 'btrl' 来清除由 'btsl' 设置的比特位。pos 的约束 "Ir" 表明 pos 位于寄存器,并且它的值为 0-31(x86 相关约束)。也就是说,我们可以设置/清除 ADDR 变量上第 0 到 31 位的任一比特位。因为条件码会被改变,所以我们将 "cc" 添加进修饰寄存器列表。

  1. 现在我们看看一些更为复杂而有用的函数。字符串拷贝。
static inline char * strcpy(char * dest,const char *src)
{
int d0, d1, d2;
__asm__ __volatile__(  "1:\tlodsb\n\t"
                       "stosb\n\t"
                       "testb %%al,%%al\n\t"
                       "jne 1b"
                     : "=&S" (d0), "=&D" (d1), "=&a" (d2)
                     : "0" (src),"1" (dest) 
                     : "memory");
return dest;
}

源地址存放于 esi,目标地址存放于 edi,同时开始拷贝,当我们到达 0 时,拷贝完成。约束 "&S"、"&D"、"&a" 表明寄存器 esi、edi 和 eax 早期修饰寄存器,也就是说,它们的内容在函数完成前会被改变。这里很明显可以知道为什么 "memory" 会放在修饰寄存器列表。

我们可以看到一个类似的函数,它能移动双字块数据。注意函数被声明为一个宏。

#define mov_blk(src, dest, numwords) \
__asm__ __volatile__ (                                          \
                       "cld\n\t"                                \
                       "rep\n\t"                                \
                       "movsl"                                  \
                       :                                        \
                       : "S" (src), "D" (dest), "c" (numwords)  \
                       : "%ecx", "%esi", "%edi"                 \
                       )

这里我们没有输出,寄存器 ecx、esi和 edi 的内容发生了改变,这是块移动的副作用。因此我们必须将它们添加进修饰寄存器列表。

  1. 在 Linux 中,系统调用使用 GCC 内联汇编实现。让我们看看如何实现一个系统调用。所有的系统调用被写成宏(linux/unistd.h)。例如,带有三个参数的系统调用被定义为如下所示的宏。
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile (  "int $0x80" \
                  : "=a" (__res) \
                  : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
                    "d" ((long)(arg3))); \
__syscall_return(type,__res); \
}

无论何时调用带有三个参数的系统调用,以上展示的宏就会用于执行调用。系统调用号位于 eax 中,每个参数位于 ebx、ecx、edx 中。最后 "int 0x80" 是一条用于执行系统调用的指令。返回值被存储于 eax 中。

每个系统调用都以类似的方式实现。Exit 是一个单一参数的系统调用,让我们看看它的代码看起来会是怎样。它如下所示。

{
        asm("movl $1,%%eax;         /* SYS_exit is 1 */
             xorl %%ebx,%%ebx;      /* Argument is in ebx, it is 0 */
             int  $0x80"            /* Enter kernel mode */
            );
}

Exit 的系统调用号是 1,同时它的参数是 0。因此我们分配 eax 包含 1,ebx 包含 0,同时通过 int $0x80 执行 exit(0)。这就是 exit 的工作原理。

8. 结束语

这篇文档已经将 GCC 内联汇编过了一遍。一旦你理解了基本概念,你就可以按照自己的需求去使用它们了。我们看了许多例子,它们有助于理解 GCC 内联汇编的常用特性。

GCC 内联是一个极大的主题,这篇文章是不完整的。更多关于我们讨论过的语法细节可以在 GNU 汇编器的官方文档上获取。类似地,要获取完整的约束列表,可以参考 GCC 的官方文档。

当然,Linux 内核大量地使用了 GCC 内联。因此我们可以在内核源码中发现许多各种各样的例子。它们可以帮助我们很多。

如果你发现任何的错别字,或者本文中的信息已经过时,请告诉我们。

9. 参考

  1. Brennan’s Guide to Inline Assembly
  2. Using Assembly Language in Linux
  3. Using as, The GNU Assembler
  4. Using and Porting the GNU Compiler Collection (GCC)
  5. Linux Kernel Source

via: http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

作者:Sandeep.S 译者:cposture 校对:wxy

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

今日关注

即将发布的 Debian Unstable 操作系统将默认采用 GCC 6 编译器以及 Binutils2.27。Debian Unstable是Debian GNU/Linux 发行版的一个滚动开发版本,目前在其软件仓库中,可以下载GCC 6.1.1, Boost 1.61 和 ICU 57 库了。

图文摘要

免费开源的跨平台 RAW 图像编辑器digiKam 5.1.0发布。这一版本除了对一些 bug 的修复之外,还增加了libraw 0.18.0库,用来读取各种数码相机拍摄出来的 RAW 图片,新的 libraw 库支持Canon 80D, Canon 1300D, Canon 1DX Mark II, Fujifilm X-Pro2, Fujifilm X70, Fujifilm X-E2S, Nikon D5, Leica M, 和 Leica X-U。Samsung Galaxy S7 的用户也可以使用 digiKam 浏览图片了。

BakAndImgCD 19.0正式发布。此版本基于4MLinux Backup Scripts 19.0,可以用来从各种Linux 操作系统、Windows 系统、Mac OSX 的文件系统进行数据备份。另外,BakAndImgCD 19.0还支持比较小众的F2FS, NILFS2, JFS, Reiser4, ReiserFS, 和 XFS文件系统。还支持从任意分区或者驱动进行写盘操作。可以下载体验了。

Claws Mail 3.14.0 发布。此版本支持通过Master Passphrase 来保存 email 帐号的密码。另外,密码的存储方法也发生了变化,所有的密码都存储到了~/.claws-mail/passwordstorerc这个独立的文件中,并采用了更强的加密算法。已经可以下载使用了。

GCC邮件列表中在争论GCC是否应该接受收费插件,但是认为GCC是一个自由软件(free software)开发的媒介的论调占得了上风

Gcc以及它在模块化方面的缺失又一次作为一个问题被提出来,并且和市场上的新的编译器LLVM做了对比。GCC巨大而古老:5百万行代码,30年研发时间,并且还在继续增长。相比较而言,LLVM更加年轻,更加模块化,并且允许所有的语言都作为一个模块添加进去。

LLVM的核心是‘开放源代码(Open Source)’。GCC是反著作权(copyleft)代表,是严格的自由软件(free software),她不允许以任何形式收费的插件的代码进入到GCC的代码中。争论的一种意见,正如Eric Raymond说的,“FSF不可能既阻止持有所有权的供应者添加他们的插件到一个免费编译器中,又让这个编译器得到发展。就像马儿已经偏离了跑道,反对插件策略的战略目标已经彻底的失败了”。

LLVM已经被苹果公司采用作为OS X和苹果硬件上GCC的替代品,并且正在变得流行起来,特别是在BSD系列操作系统的用户中间。LLVM的拥护者推测LLVM将会在更广阔的应用程序和移动设备开发市场上成为GCC的替代者。GCC的反对者们的观点是GCC太过复杂,并且开发者们必须遵守她的‘反著作权(copyleft)’。这限制了那些不想在‘反著作权(copyleft)’许可证下发布他们的语言或者软件产品。作为典型,苹果公司有一个很长的厌恶自由软件的历史。他们也不允许遵守‘反著作权(copyleft)’的软件通过他们的App Store发布。

LLVM和GCC之间的争论其实是GNU/Linux和BSD系列、开放源代码和自由软件之间历史差异的翻新版。开放源码的开发者允许代码被以任何形式的使用,免费或者维持版权。自由软件则严格地规定,代码或者针对代码做的更新,必须保持永久免费。自由软件的支持者认为完整的‘反著作权’授权有助于GCC的发展,并且已经将Linux和自由软件带到一个其他方式无法到达的高度,同时保证了自由软件不会被收购或者堕落成商业利益。开放源码的支持者则认为开放源码更加的自由,因为使用这没有受到限制,他们可以随意使用,包括开发非开源的版本。

GNU编译器集合(GCC)一直是自由软件发展的关键。编译器是稀有且昂贵的商品,版权软件公司也充斥着对不符合标准的特性的需求。让软件兼容不同的机器和操作系统是一个非必需的复杂任务。GCC作为第一个真正免费的跨平台编译器,简化了这个过程。

GCC对于软件开发者和移动设备开发者来说也是一个划时代的产品,而不仅仅对于那些自由软件概念提出者。GCC不但免费和可移植,她跨越不同硬件架构的普遍性和公用性使得更加容易做到软件的兼容性、鲁棒性和一致性。这和John Gilmore,Michael Tiemann和David Henkel-Wallace在开发GCC时发现的一样。这也是Cygnus Solutions公司主要的卖点,Cygnus Solutions是第一家靠卖自由软件赚钱的公司。[译注:Cygnus Solutions是John Gilmore, Michael Tiemann and David Henkel- Wallace创办的公司,同时也是GNU几个主要产品的贡献者]

LLVM和GCC之间主要的技术差异集中在组成‘前端’,‘中端’,‘后端’的模块分割。‘前端’用来翻译特定的语言。‘中端’对翻译后产生的代码进行优化。‘后端’将优化后的代码转化成特定硬件架构的机器码。LLVM将这些模块分割成不同的实体,但是由于语义的和历史的原因,GCC模糊了这些模块之间的界限。

对于一个自由软件项目,添加一种新的语言或者架构到GCC也许是一个非常困难的过程,添加有版权的插件也是不允许的。由于模块间界限非常不明确,最容易的添加方法就是让添加的特性遵循自由软件许可证。最初的开发者也许想保持代码的封闭和版权,但最后不得不将代码以自由软件发布。早期的C++以及Objective C就被认为是其中典型的例子。

与此相反,LLVM允许,甚至也许可以说是鼓励添加和发展版权语言和架构,比如英伟达基于Clang和LLVM的对于GPU开发的NVCC。NVCC的源代码是自由软件或者开源软件开发者获取不到的。

Richard Stallman对这方面的演讲中旗帜鲜明地宣布:“在自由软件运动中,我们为自由而战。自由软件的的价值观从根本上就和开源软件不同,后者以写‘更好的代码’为终极目标。如果GCC从免费的编译器变成非免费的编译器,她将不再能够达成自由的目标。

“Clang和LLVM的开发者不认可我们的价值观和目标,所以得出了跟我们不一样的结论。他们反对我们采取的捍卫自由的措施,因为他们只看到这对他们造成的不便,却没有看到(或者不关心)他们真正的需求。我猜测他们把他们的工作定义为‘开源’,并且漠视自由。”

GCC开发者们不可能在许可证的条件上妥协。LLVM在某些行业的部门非常流行,因为它很年轻很新,在编程语言的浪潮中跳跃式发展着。流行的风向着更加开放奔跑,GCC决心跟商业利益死磕也许是这个长期演进路上的一大助力。Unix公司们从80和90年代的Unix战争中学到了一些东西。语言和操作系统都是工具,它们最好是开放和共享的。GCC是自由软件,不属于任何人。


via: http://www.linuxuser.co.uk/features/staying-free-should-gcc-allow-non-free-plug-ins

译者:love\_daisy\_love 校对:wxy

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