16 de nov de 2014

GCC Tutorial? Or at least what I think I know about it...

Disclaimer (em inglês... vede a versão em português mais abaixo)


You may have noticed this post is in English (which is not the norm here xP -- if you want to read the Portuguese version, just scroll down). From now on, I intend to always write an English version of my posts and put them in the end, after the Portuguese part [the order of this one is an exception]. I don't believe anyone will become more interested in reading my blog because of that, but maybe this will allow me to refer some friends to here if I think I wrote something they'd be interested in. Don't expect the English version, however, to be as "high" in grammar (with "noble" verb conjugations and syntax constructions) as the Portuguese one: only now I've been trying to "introduce myself" to English literature ><

Contextual motivation...


At some point in the relativelly near past I talked to some friends about the fact that I don't like IDEs to do my job: I often can't configure them as much as I would like (or at least take a lot of time to find out how) and I simply don't trust them to automatically build a "project" to me. This is an opinion I've been "developing" since the times when I tried (and failed miserably) to use Eclipse: it had so many options and was so polluted with so many buttons that I never found what I wanted to do. Going in the "opposite direction", I ended up using Vim, which is now definitely my preferred text editor.

But with Vim I need to compile things "by hand", either by creating a Makefile or by typing the command line. I'll try to describe more os less the [very simple] steps one should take to compile a program in C using GCC.

Solely compiling

Let's suppose you have a file called main.c which has the code of your program. For simplicity, let's assume this is the only file your program has. The program below is a fancy way to print "Hello World!" that a professor of my University posted in Facebook some days ago:


Now imagine that you want to compile this program. There is a problem with the word compiling: it is overloaded with two meanings. The first and most obvious one is something like "create a program out of this code". To create such a program, you need to do two things: compiling (here is the second meaning) and linking. Here, I'll try to use compiling and linking when referring to the first meaning, to avoid the ambiguity.

But what is compiling then? (i.e., in the second meaning)

In this context, to compile a code is to transform the code into a binary object file. This file has all your program functions, but they are separate, i.e., "independent". The linking step "connects" (links?) the function calls and generates an executable (or a library, if that's what you want).

To compile a program, write in your shell
$ gcc -c main.c
An object file named main.o will be output. You can do this to any .c file, but you normally don't do this to .h files. This is because the .h files are actually copied into the .c files by the preprocessor before the compilation is done (that's what those "#include" lines in program are supposed to do). The compiler, therefore, doesn't even know about the existence of .h files.

Another thing that the compiler doesn't care about is/are function calls. As I said, each function is compiled separately and function calls are only "referenced". It needs, however, to see a function declaration (i.e., only the function header -- the "prototype" of the function), so that it knows if the types passed in the function call are right. That's why, if two functions reference each other, you need to write the prototype of one of them at the top. For example:


You should not care about what the functions do, but at least note that you will get a compilation error if you don't write the function prototype of function2 before calling it.


Now imagine that your main.c actually includes a file called helloworld.h that has a function declaration in it, and imagine that this function is defined in helloworld.c. Let's rewrite our code to see the three files:


Remember that, because helloworld.h is actually copied into main.c (because of the #include "helloworld.h" line inside the file), the compiler sees the function declaration (i.e., the header, aka the prototype) of helloWorld(). That is all the compiler needs to know to create the object file main.o.


Linking

But creating the main.o file is not creating the executable binary. You still need to link your code. Before doing that, you'll need to create one .o file for each .c file you have. In our case, we want the following calls:
$ gcc -c main.c
$ gcc -c helloworld.c
Linking is very simple: just take all the .o files and call gcc on it. Additionally, you can specify the -o option, to select the name of your executable. If you don't, the default executable file name will be a.out.
$ gcc main.o helloworld.o
$ gcc main.o helloworld.o -o helloworld
There is one problem though: if you try the line above, you will get some linker errors. The reason is that you need to specify a -l option. This is something you have to do sometimes when you are using some specific library. In our case, since we are using the math library, we need -lm, but the letters after -l can vary: sometimes you need -lX11, sometimes you need -lpthread, ...

The final command line is:
$ gcc main.o helloworld.o -lm -o helloworld

 

Compiling and linking

Ok, but it is true that, if you only have one file of code, you can simply do
$ gcc main.c -o main
I gave more details because programs very rarely have only one code file. Because typing is often very tiresome (and error prone), you are probably going to want to create a Makefile. Maybe I'll do a post on it in the future =)

R$

--

Ressalva (Disclaimer?)

Podeis ter notado que essa publicação está em inglês (o que não é a norma por aqui xP -- se quiserdes ler a versão em português, apenas olhai abaixo). A partir de agora, pretendo sempre escrever uma versão em inglês das minhas postagens e pô-las no fim, depois da parte em português [essa aqui é uma exceção quanto à ordem]. Não acredito que ninguém tornar-se-á mais interessado em ler meu blog por causa disso, mas talvez isso me permita referir o blog a alguns amigos se eu pensar que escrevi algo em que eles estariam interessados. Não espereis que a versão inglesa, porém, seja tão "alta" em gramática (com "nobres" conjugações verbais e construções sintáticas) quanto a em português: somente agora tenho começado a "me introduzir" à literatura inglêsa ><


Motivação Contextual...

Em algum momento no passado relativamente próximo eu conversei com alguns amigos sobre o fato de eu não gostar de que IDEs façam meu trabalho: eu freqüentemente não consigo configurá-las tanto quanto eu gostaria (ou pelo menos tomo um monte de tempo para descobrir como fazê-lo) e simplesmente não confio nelas para automaticamente construir (build?) um "projeto" para mim. Essa é uma opinião que venho desenvolvendo desde os tempos quando eu tentei (e falhei miseravelmente) usar o Eclipse: ele tinha tantas opções e era tão poluído com muitos botões que eu nunca encontrava o que eu queria fazer. Tendo ido na "direção oposta", eu acabei usando o Vim, que é definitivamente meu editor de texto preferido.

Mas com o Vim eu preciso compilar coisas "à mão", ou através da criação de um Makefile ou através da linha de comando. Eu tentarei descrever mais ou menos os passos [bem simples] que se precisa tomar para compilar um programa em C usando o GCC.

Apenas Compilando

Suponhamos que tenhais um arquivo chamado main.c que tem o código de vosso programa. Para simplificar, assumamos que esse é o único arquivo que vosso programa tem. O programa abaixo é uma forma camanguenta [minha família usa essa palavra pra indicar que a pessoa "manja dos paranauês"] de imprimir "Hello World!" na tela, que um professor da minha Universidade postou no Facebook alguns dias atrás:



Agora imaginai que quereis compilar esse programa. Tem um problema com a palavra compilar: ela é sobrecarregada com dois significados. O primeiro e mais óbvio é algo como "criar um programa a partir desse código". Para criar tal programa, precisais fazer duas coisas: compilar (aqui está o segundo significado) e linkar. Aqui, tentarei usar compilar e linkar para me referir ao primeiro significado, para evitar a ambiguidade.

Mas o que é compilar então? (i.e., no segundo significado)

Nesse contexto, compilar um código é transformar o código num arquivo binário "objeto". Esse arquivo tem todas as funções do vosso programa, mas elas estão separadas, i.e., "independentes". O passo de linkagem "conecta" (linka?) as chamadas de função e gera um executável (ou uma biblioteca, se é isso que quereis).

Para compilar um programa, escrevei em vossa linha de comando
$ gcc -c main.c
Um arquivo objeto chamado main.o será criado. Pode-se fazer isso com qualquer arquivo .c, mas normalmente não querereis fazê-lo com arquivo .h. Isso porque os arquivos .h são na verdade copiados pra dentro dos .c pelo pré-processador antes da compilação ocorrer (é pra isso que aquelas linhas com "#include" no programa servem). O compilador, portanto, nem mesmo sabe sobre a existência dos arquivos .h.

Uma outra coisa com que o compilador não se importa são as chamadas de função. Como eu disse, cada função é compilada separadamente e chamadas de função são apenas "referenciadas". Ele precisa, porém, ver as declarações de função (i.e., apenas o cabeçalho da função -- o "protótipo" da função), para saber se os tipos das variáveis passadas como parâmetro na chamada de função estão corretos. É por isso que, se duas funções se referenciam uma à outra, precisais escrever o protótipo de uma delas antes da declaração das duas. Por exemplo:



Não quero que vos importeis com o que as funções fazem, mas ao menos que noteis que teríeis um erro de compilação se não escrevêsseis o protótipo da function2 antes de chamá-la.

Agora imaginai que o vosso main.c na verdade "includa" um arquivo chamado helloworld.h que tem uma declaração de função dentro, e imaginai que essa função é definida em helloworld.c. Reescrevamos nosso código para ver os três arquivos:



Lembrai que, porque helloworld.h é na verdade copiado para dentro do main.c (por causa da linha #include "helloworld.h" dentro do arquivo), o compilador vê a declaração de função (i.e., o cabeçalho, também conhecido como protótipo) de helloworld(). Isso é tudo que o compilador precisa pra gerar o arquivo objeto main.o.


Linkando (ligando?)

Mas criar o arquivo main.o não é criar o binário executável. Ainda precisais linkar vosso código. Antes de fazê-lo, precisareis criar um arquivo .o para cada arquivo .c que tendes. No nosso caso, queremos as seguintes chamadas:
$ gcc -c main.c
$ gcc -c helloworld.c
Linkar é muito simples: apenas pegai todos os arquivos .o e chamai gcc neles. Adicionalmente, podeis especificar a opção -o, para selecionar o nome de vosso executável. Se não o fizerdes, o nome padrão do arquivo executável será a.out.

$ gcc main.o helloworld.o
$ gcc main.o helloworld.o -o helloworld
Há, porém, um problema: se tentardes as linhas acima, vereis alguns erros de linkagem. O motivo é que precisais especificar uma opção -l. Isso é algo que se precisa fazer às vezes quando se está usando bibliotecas específicas. No nosso caso, já que estamos usando a biblioteca math, precisamos de -lm, mas as letras depois de -l podem variar: às vezes precisareis de -lX11, às vezes precisareis de -lpthread, ...

A linha de comando final é:
$ gcc main.o helloworld.o -lm -o helloworld

Compilando e Linkando

Ok, mas é verdade que, se tendes somente um arquivo de código, podeis simplesmente escrever
$ gcc main.c -o main
Dei mais detalhes porque programas têm só um arquivo de código somente raramente. Já que escrever essas coisas na linha de comando é bastante cansativo (e suscetível a erros), provavelmente querereis criar um Makefile. Talvez eu faça uma postagem sobre isso no futuro =)

R$

Nenhum comentário:

Postar um comentário