GCC входить до складу будь-якого дистрибутива Linuxі, як правило, встановлюється за умовчанням. Інтерфейс GCC, це стандартний інтерфейс компілятора на UNIX платформі, що сягає своїм корінням в кінець 60-х, початок 70-х років минулого століття - інтерфейс командного рядка. Не варто лякатися, за минулий час механізм взаємодії з користувачем був відточений до можливої ​​в даному випадку досконалості, і працювати з GCC (за наявності кількох додаткових утиліт та путнього текстового редактора) простіше, ніж з будь-яким із сучасних візуальних IDE. Автори набору постаралися максимально автоматизувати процес компіляції та складання додатків. Користувач викликає керуючу програму gcc, вона інтерпретує передані аргументи командного рядка (опції та імена файлів) і для кожного вхідного файлу, відповідно до використаної мови програмування, запускає свій компілятор, потім, якщо це необхідно, gccавтоматично викликає асемблер і лінковщик (компонувальник).

Цікаво, компілятори є одними з небагатьох програм UNIX для яких не байдуже розширення файлів. По розширенню GCC визначає, що за файл перед ним і що з ним потрібно (можна) зробити. Файли вихідного коду мовою Cповинні мати розширення .c на мові C++, як варіант, .cpp , заголовні файли мовою C.h, об'єктні файли .o і так далі. Якщо використовувати неправильне розширення, gccбуде працювати не коректно (якщо взагалі погодитися, щось робити).

Перейдемо до практики. Напишемо, відкомпілюємо і виконаємо якусь нехитру програму. Не будемо оригінальнувати, як вихідний файл приклад програми на мові Cстворимо файл з таким вмістом:

/* hello.c */

#include

Main( void )
{

Printf("Hello World \n");

return 0 ;

Тепер у каталозі c hello.c віддамо команду:

$gcc hello.c

Через кілька часток секунди в каталозі з'явиться файл a.out:

$ls
a.out hello.c

Це і є готовий файл нашої програми. За замовчуванням gccнадає вихідному виконуваному файлу ім'я a.out (колись дуже давно це ім'я означало assembler output).

$file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), динамічно linked (uses shared libs), для GNU/Linux 2.6.15, не stripped

Запустимо програмний продукт, що вийшов:

$./a.out
Hello World


Чому в команді запуску виконання файлу з поточного каталогу необхідно явно вказувати шлях до файлу? Якщо шлях до файлу, що виконується, не вказаний явно, оболонка, інтерпретуючи команди, шукає файл у каталогах, список яких заданий системною змінною PATH .

$echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

Каталоги у списку розділені символом двокрапки. При пошуку файлів, оболонка переглядає каталоги в порядку, в якому вони перераховані в списку. За умовчанням, з міркувань безпеки поточний каталог . до списку не внесено, відповідно, оболонка виконувані файли шукати в ньому не буде.

Чому не рекомендується вносити. в PATH? Вважається, що в реальній розрахованій на багато користувачів системі завжди знайдеться який-небудь поганий людина, який розмістить в загальнодоступному каталозі шкідливу програмуз ім'ям виконуваного файлу, що збігається з ім'ям якоїсь команди, що часто викликається місцевим адміністратором з правами суперкористувача... Змова вдасться якщо . стоїть на початку списку каталогів.


Утиліта fileвиводить інформацію про тип (з погляду системи) переданого в командному рядку файлу, для деяких типів файлів виводить будь-які додаткові відомостіщо стосуються вмісту файлу.

$file hello.c
hello.c: ASCII C program text
$file annotation.doc
annotation.doc: CDF V2 Document, Little Endian, Os: Windows, Version 5.1, page page: 1251, Autor: MIH, Template: Normal.dot, Last Saved By: MIH, Revision Number: 83, Name of Creating Application: Microsoft Office Word, Total Editing Time: 09:37:00, Last Printed: Thu Jan 22 07:31:00 2009, Create Time/Date: Mon Jan 12 07:36:00 2009, Last Saved Time/Date: Thu Jan 22 07: 34:00 2009, Кількість сторінок: 1, Кількість Words: 3094, Номер Characters: 17637, Security: 0

Ось, власне, і все, що потрібно від користувача для успішного застосування. gcc :)

Ім'я вихідного виконуваного файлу (як і будь-якого іншого файлу, що формується gcc) можна змінити за допомогою опції -o:

$ gcc -o hello hello.c
$ls
hello hello.c
$./hello
Hello World


У прикладі функція main() повертає здавалося б нікому непотрібне значення 0 . У UNIX-подібних системах, після завершення роботи програми, прийнято повертати в командну оболонку ціле число - у разі успішного завершення нуль, будь-яке інше інакше. Інтерпретатор оболонки автоматично надасть отримане значення змінного середовища з ім'ям ? . Переглянути її вміст можна за допомогою команди echo$? :

$./hello
Hello World
$echo$?
0

Вище було сказано, що gccце програма, що керує, призначена для автоматизації процесу компіляції. Подивимося, що насправді відбувається в результаті виконання команди gcc hello.c.

Процес компіляції можна розбити на 4 основні етапи: обробка препроцесором, власне компіляція, асемблювання, лінковка (зв'язування).

Опції gccдозволяють перервати процес на будь-якому з цих етапів.

Препроцесор здійснює підготовку вихідного файлу до компіляції - вирізує коментарі, додає вміст заголовних файлів (директива препроцесора #include), реалізує розкриття макросів (символічних констант, директива препроцесора #define).

Скориставшись опцією -Eподальші дії gccможна перервати та переглянути вміст файлу, обробленого препроцесором.

$ gcc -E -o hello.i hello.c
$ls
hello.c hello.i
$less hello.i
. . .
# 1 "/usr/include/stdio.h" 1 3 4
# 28 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
. . .
typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
. . .
extern int printf (__const char *__restrict __format, ...);
. . .
# 4 "hello.c" 2
main (void)
{
printf ("Hello World\n");
return 0;
}

Після обробки препроцесором вихідний текст нашої програми розбух і набув не легкочитаний вигляд. Код, який ми колись власноруч набили, звівся до кількох рядків наприкінці файлу. Причина - підключення файлу заголовка стандартної бібліотеки C. Заголовковий файл stdio.h сам по собі містить багато всього різного та ще вимагає включення інших заголовних файлів.

Зверніть увагу на розширення файлу hello.i. За угодами gccрозширення .i відповідає файлам з вихідним кодом мовою Cне вимагає обробки препроцесором. Такі файли компілюються минаючи препроцесор:

$ gcc -o hello hello.i
$ls
hello hello.c hello.i
$./hello
Hello World

Після препроцессингу настає черга компіляції. Компілятор перетворює вихідний текст програми мовою високого рівня в код мовою асемблера.

Значення слова компіляція розмито. Вікіпедисти, наприклад, вважають, посилаючись на міжнародні стандарти, що компіляція це "перетворення програмою-компілятором вихідного тексту будь-якої програми, написаного мовою програмування високого рівня, у мову, близьку до машинного, чи об'єктний код." У принципі це визначення нам підходить, мова асемблера справді ближче до машинного, ніж C. Але в повсякденному житті під компіляцією найчастіше розуміють просто будь-яку операцію, що перетворює вихідний код програми якоюсь мовою програмування у виконуваний код. Тобто процес, що включає всі чотири зазначених вище етапи також може бути названий компіляцією. Подібна неоднозначність є і в цьому тексті. З іншого боку, операцію перетворення вихідного тексту програми в код мовою асемблера можна позначити і словом трансляція - "перетворення програми, представленої однією з мов програмування, на програму іншою мовою і, певному сенсі, рівносильну першої".

Зупинити процес створення виконуваного файлу після завершення компіляції дозволяє опція -S:

$ gcc -S hello.c
$ls
hello.c hello.s
$file hello.s
hello.s: ASCII assembler program text
$less hello.s
.file "hello.c"
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $.LC0, (% esp)
call puts
movl $0, %eax
leave
ret
.size main, .-main


У каталозі з'явився файл hello.s, що містить реалізацію програми мовою асемблера. Зверніть увагу, задавати ім'я вихідного файлу за допомогою опції -oв даному випадку не знадобилося, gccавтоматично його згенерував, замінивши в імені вихідного файлу розширення .c на .s. Для більшості основних операцій gccІм'я вихідного файлу формується шляхом подібної заміни. Розширення .s стандартне для файлів з вихідним кодом мовою асемблера.

Отримати код, що виконується, можна і з файлу hello.s :

$ gcc -o hello hello.s
$ls
hello hello.c hello.s
$./hello
Hello World

Наступний етап операція асмеблювання - трансляція коду мовою асемблера в машинний код. Результат операції – об'єктний файл. Об'єктний файл містить блоки готового до виконання машинного коду, блоки даних, а також список визначених у файлі функцій та зовнішніх змінних ( таблицю символів ), але при цьому в ньому не задані абсолютні адреси посилань на функції та дані. Об'єктний файл не може бути запущений на виконання безпосередньо, але надалі (на етапі лінківки) може бути об'єднаний з іншими об'єктними файлами (при цьому, відповідно до таблиць символів, будуть обчислені та заповнені адреси існуючих між файлами перехресних посилань). Опція gcc-c , зупиняє процес після завершення етапу асемблювання:

$ gcc -c hello.c
$ls
hello.c hello.o
$file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), не stripped

Для об'єктних файлів прийнято стандартне розширення .o.

Якщо отриманий об'єктний файл hello.o передати лінковнику, останній обчислить адреси посилань, додасть код запуску та завершення програми, код виклику бібліотечних функцій і в результаті ми матимемо готовий виконуваний файл програми.

$ gcc -o hello hello.o
$ls
hello hello.c hello.o
$./hello
Hello World

Те, що ми зараз зробили (вірніше gccпроробив за нас) і є зміст останнього етапу – лінковки (зв'язування, компонування).

Ну от мабуть про компіляцію і все. Тепер торкнемося деяких, на мій погляд важливих опцій gcc.

Опція -I шлях/до/каталогу/з/заголовними/файлами - додає вказаний каталог до списку шляхів пошуку файлів заголовків. Каталог, доданий опцією -Iпроглядається першим, потім пошук продовжується у стандартних системних каталогах. Якщо опцій -Iдекілька, задані ними каталоги проглядаються зліва направо, у міру появи опцій.

Опція -Wall- виводить попередження, викликані потенційними помилками в коді, які не перешкоджають компіляції програми, але здатні призвести, на думку компілятора, до тих чи інших проблем під час її виконання. Важлива та корисна опція, розробники gccрекомендують користуватися ним завжди. Наприклад, маса попереджень буде видана при спробі компіляції такого файлу:

1 /* remark.c */
2
3 static int k = 0;
4 static int l( int a);
5
6 main()
7 {
8
9 int a;
10
11 int b, c;
12
13 b + 1;
14
15 b = c;
16
17 int*p;
18
19 b = * p;
20
21 }


$ gcc -o remark remark.c
$ gcc -Wall -o remark remark.c
remark.c:7: warning: return type defaults to ‘int’

remark.c:13: warning: statement with no effect
remark.c:9: warning: unused variable 'a'
remark.c:21: warning: control reaches end of non-void function
remark.c: At top level:
remark.c:3: warning: ‘k’ defined but not used
remark.c:4: warning: 'l' declared 'static' but never defined
remark.c: In function 'main':
remark.c:15: warning: ‘c’ is used uninitialized in this function
remark.c:19: warning: ‘p’ is used uninitialized in this function

Опція -Werror- перетворює всі попередження на помилки. У разі появи попередження перериває процес компіляції. Використовується спільно з опцією-Wall.

$ gcc -Werror -o remark remark.c
$ gcc -Werror -Wall -o remark remark.c
cc1: warnings being treated as errors
remark.c:7: error: return type defaults to ‘int’
remark.c: In function 'main':
remark.c:13: error: statement with no effect
remark.c:9: error: unused variable 'a'

Опція -g- поміщає в об'єктний чи виконуваний файл інформацію необхідну роботи відладчика gdb. При складанні будь-якого проекту з метою подальшого налагодження, опцію -gнеобхідно включати як на етапі компіляції, так і на етапі компонування.

Опції -O1, -O2, -O3- задають рівень оптимізації коду, що генерується компілятором. Зі збільшенням номера ступінь оптимізації зростає. Дія опцій можна побачити на такому прикладі.

Вихідний файл:

/* circle.c */

Main( void )
{

int i;

for(i = 0; i< 10 ; ++i)
;

return i;

Компіляція з рівнем оптимізації за замовчуванням:

$ gcc -S circle.c
$less circle.s
.file "circle.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $0, -4(%ebp)
jmp .L2
.L3:
addl $1, -4(%ebp)
.L2:
cmpl $9, -4(%ebp)
jle .L3
movl -4(%ebp), %eax
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits

Компіляція з максимальним рівнем оптимізації:

$ gcc-S-O3 circle.c
$less circle.s
.file "circle.c"
.text
.p2align 4,15
.globl main
.type main, @function
main:
pushl %ebp
movl $10, %eax
movl %esp, %ebp
popl %ebp
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits

У другому випадку в отриманому коді навіть немає натяку на якийсь цикл. Дійсно, значення i можна обчислити ще на етапі компіляції, що і було зроблено.

На жаль, для реальних проектів різниця у продуктивності при різних рівнях оптимізації практично не помітна.

Опція -O0- скасовує будь-яку оптимізацію коду. Опція необхідна на етапі налагодження програми. Як було показано вище, оптимізація може призвести до зміни структури програми до невпізнання, зв'язок між виконуваним та вихідним кодом не буде явним, відповідно, покрокове налагодження програми буде неможливим. При включенні опції -g, рекомендується включати та -O0.

Опція -Os- задає оптимізацію за ефективності коду, а, по розміру одержуваного файла. Продуктивність програми при цьому має бути порівнянна з продуктивністю коду, отриманого при компіляції з рівнем оптимізації заданим за умовчанням.

Опція -march = architecture- Задає цільову архітектуру процесора. Список підтримуваних архітектур великий, наприклад, для процесорів сімейства Intel/AMDможна поставити i386, pentium, prescott, opteron-sse3і т.д. Користувачі бінарних дистрибутивів повинні мати на увазі, що для коректної роботи програм із зазначеною опцією бажано, щоб і всі бібліотеки, що підключаються, були відкомпільовані з тією ж опцією.

Про опції переданих лінковщику буде сказано нижче.

Невеликий додаток:

Вище було сказано, що gccвизначає тип (мова програмування) переданих файлів з їхнього розширення і, відповідно до вгаданого типу (мови), робить дії над ними. Користувач зобов'язаний стежити за розширеннями створюваних файлів, вибираючи їх так, як того вимагають угоди gcc. В дійсності gccможна підсовувати файли із довільними іменами. Опція gcc-xдозволяє явно вказати мову програмування файлів, що компілюються. Дія опції поширюється на всі наступні перелічені у команді файли (аж до появи наступної опції -x). Можливі аргументи опції:

c c-header c-cpp-output

c++ c++-header c++-cpp-output

objective-c objective-c-header objective-c-cpp-output

objective-c++ objective-c++-header objective-c++-cpp-output

assembler assembler-with-cpp

ada

f77 f77-cpp-input

f95 f95-cpp-input

java

Призначення аргументів має бути зрозумілим з їх написання (тут cppне має жодного відношення до C++, це файл вихідного коду (заздалегідь оброблений препроцесором). Перевіримо:

$mv hello.c hello.txt
$ gcc -Wall -x c -o hello hello.txt
$./hello
Hello World

Роздільна компіляція

Сильною стороною мов C/C++є можливість розділяти вихідний код програми з кількох файлів. Навіть можна сказати більше – можливість роздільної компіляції – це основа мови, без неї ефективне використання. Cне мислимо. Саме мультифайлове програмування дозволяє реалізувати Cвеликі проекти, наприклад, такі як Linux(тут під словом Linuxмається на увазі як ядро, і система загалом). Що дає окрема компіляція програмісту?

1. Дозволяє зробити код програми (проекту) більш зручним для читання. Файл вихідника на кілька десятків екранів стає практично неосяжним. Якщо, згідно з якоюсь (заздалегідь продуманою) логікою, розбити його на ряд невеликих фрагментів (кожен в окремому файлі), впоратися зі складністю проекту буде набагато простіше.

2. Дозволяє скоротити час повторної компіляції проекту. Якщо зміни внесені в один файл, немає сенсу перекомпілювати весь проект, достатньо заново відкомпілювати тільки цей змінений файл.

3. Дозволяє розподілити роботу над проектом між кількома розробниками. Кожен програміст творить і налагоджує свою частину проекту, але в будь-який момент можна буде зібрати (перезбирати) всі напрацювання, що виходять, в кінцевий продукт.

4. Без роздільної компіляції не було б бібліотек. За допомогою бібліотек реалізовано повторне використання та розповсюдження коду на C/C++, причому бінарного коду, що дозволяє з одного боку надати розробникам простий механізм включення його в свої програми, з іншого боку приховати від них конкретні деталі реалізації. Працюючи над проектом, завжди варто замислюватися над тим, а чи не знадобиться щось із вже зробленого колись у майбутньому? Може варто заздалегідь виділити та оформити частину коду як бібліотеку? На мою думку, такий підхід суттєво спрощує життя і економить масу часу.

GCC, зрозуміло, підтримує роздільну компіляцію, причому вимагає від користувача якихось спеціальних вказівок. Загалом, все дуже просто.

Ось практичний приклад (правда дуже і умовний).

Набір файлів вихідного коду:

/* main.c */

#include

#include "first.h"
#include "second.h"

int main( void )
{

First();
second();

Printf("Main function... \n");

return 0 ;


/* first.h */

void first( void );


/* first.c */

#include

#include "first.h"

void first( void )
{

Printf("First function... \n");


/* second.h */

void second( void );


/* second.c */

#include

#include "second.h"

void second( void )
{

Printf("Second function... \n");

Загалом маємо ось що:

$ls
first.c first.h main.c second.c second.h

Все це господарство можна скомпілювати в одну команду:

$ gcc -Wall -o main main.c first.c second.c
$./main
First function...
Second function...
Main function...

Тільки це не дасть нам практично жодних бонусів, ну за винятком більш структурованого і легкочитаемого коду, рознесеного по декількох файлах. Усі перелічені вище переваги з'являться у разі такого підходу до компіляції:

$gcc-Wall-c main.c
$gcc-Wall-c first.c
$gcc-Wall-c second.c
$ls
first.c first.h first.o main.c main.o second.c second.h second.o
$ gcc -o main main.o first.o second.o
$./main
First function...
Second function...
Main function...

Що ми зробили? З кожного вихідного файлу (компілюючи з опцією -c) отримали об'єктний файл. Потім об'єктні файли об'єднали в підсумковий виконуваний. Зрозуміло команд gccстало більше, але в ручну ніхто проекти не збирає, для цього є утиліти збирачі (найпопулярніша make). При використанні утиліт збирачів і виявляться всі з перерахованих вище переваг роздільної компіляції.

Виникає питання: як лінковник примудряється збирати об'єктні файли, правильно обчислюючи адресацію дзвінків? Звідки він взагалі дізнається, що файл second.o містить код функції second() , а коді файла main.o присутній її виклик? Виявляється все просто - в об'єктному файлі є так звана таблиця символів , що включає імена деяких позицій коду (функцій та зовнішніх змінних). Лінковщик переглядає таблицю символів кожного об'єктного файлу, шукає загальні (з збігаються іменами) позиції, на підставі чого робить висновки про фактичне місцезнаходження коду функцій (або блоків даних), що використовуються і, відповідно, здійснює перерахунок адрес дзвінків у виконуваному файлі.

Переглянути таблицю символів можна за допомогою утиліти nm.

$nm main.o
U перша
00000000 T main
U puts
U second
$nm first.o
00000000 T перша
U puts
$nm second.o
U puts
00000000 T second

Поява виклику puts пояснюється використанням функції стандартної бібліотеки printf() , що перетворилася на puts() на етапі компіляції.

Таблиця символів прописується у об'єктний, а й у виконуваний файл:

$ nm main
08049f20 d _DYNAMIC
08049ff4 d _GLOBAL_OFFSET_TABLE_
080484fc R _IO_stdin_used
w _Jv_RegisterClasses
08049f10 d __CTOR_END__
08049f0c d __CTOR_LIST__
08049f18 D __DTOR_END__
08049f14 d __DTOR_LIST__
08048538 r __FRAME_END__
08049f1c d __JCR_END__
08049f1c d __JCR_LIST__
0804a014 A __bss_start
0804a00c D __data_start
080484b0 t __do_global_ctors_aux
08048360 t __do_global_dtors_aux
0804a010 D __dso_handle
w __gmon_start__
080484aa T __i686.get_pc_thunk.bx
08049f0c d __init_array_end
08049f0c d __init_array_start
08048440 T __libc_csu_fini
08048450 T __libc_csu_init
U [email protected]@ GLIBC_2.0
0804a014 A _edata
0804a01c A _end
080484dc T _fini
080484f8 R _fp_hw
080482b8 T _init
08048330 T _start
0804a014 b completed.7021
0804a00c W data_start
0804a018 b dtor_idx.7023
0804840c T перша
080483c0 t frame_dummy
080483e4 T main
U [email protected]@ GLIBC_2.0
08048420 T second

Включення таблиці символів у виконуваний файл, зокрема, необхідно для спрощення налагодження. У принципі для виконання програми вона не дуже потрібна. Для виконуваних файлів реальних програм, З безліччю визначень функцій і зовнішніх змінних, що задіють купу різних бібліотек, таблиця символів стає досить великою. Для скорочення розмірів вихідного файлу її можна видалити, скориставшись опцією gcc-s.

$ gcc -s -o main main.o first.o second.o
$./main
First function...
Second function...
Main function...
$ nm main
nm: main: no symbols

Необхідно відзначити, що в ході компонування, лінковник не робить жодних перевірок контексту виклику функцій, він не стежить ні за типом значення, що повертається, ні за типом і кількістю прийнятих параметрів (та йому і не звідки взяти таку інформацію). Усі перевірки коректності викликів мають бути зроблені на етапі компіляції. У разі мультифайлового програмування для цього необхідно використовувати механізм заголовних файлів мови C.

Бібліотеки

Бібліотека – у мові C, файл містить об'єктний код, який може бути приєднаний до програми, що використовує бібліотеку, на етапі лінківки. Фактично бібліотека це набір особливо скомпонованих об'єктних файлів.

Призначення бібліотек – надати програмісту стандартний механізм повторного використання коду, причому механізм простий та надійний.

З точки зору операційної системи та прикладного програмного забезпечення бібліотеки бувають статичними і розділяються (динамічними ).

Код статичних бібліотек включається до складу виконуваного файлу під час лінкування останнього. Бібліотека виявляється "зашитою" у файл, код бібліотеки "зливається" з рештою коду файлу. Програма, яка використовує статичні бібліотеки, стає автономною і може бути запущена практично на будь-якому комп'ютері з підходящою архітектурою та операційною системою.

Код бібліотеки, що розділяється, завантажується і підключається до коду програми операційною системою, за запитом програми в ході її виконання. До складу файлу програми код динамічної бібліотеки не входить, у виконуваний файл включається тільки посилання на бібліотеку. В результаті, програма використовує бібліотеки, що розділяються, перестає бути автономною і може бути успішно запущена тільки в системі де задіяні бібліотеки встановлені.

Парадигма бібліотек, що розділяються, надає три істотні переваги:

1. Багаторазово скорочується розмір файлу, що виконується. У системі, що включає безліч бінарних файлів, що використовують той самий код, відпадає необхідність зберігати копію цього коду для кожного виконуваного файлу.

2. Код бібліотеки, що розділяється, використовується декількома додатками зберігатися в оперативній пам'яті в одному примірнику (насправді не все так просто...), в результаті скорочується потреба системи в доступній оперативній пам'яті.

3. Відпадає необхідність перезбирати кожен виконуваний файл у разі внесення змін до коду спільної для них бібліотеки. Зміни та виправлення коду динамічної бібліотеки автоматично відобразяться на кожній програмі, що її використовують.

Без парадигми бібліотек, що розділяються, не існувало б прекомпільованих (бінарних) дистрибутивів. Linux(Так ні яких би не існувало). Представте розміри дистрибутива, у кожний бінарний файл якого було б поміщено код стандартної бібліотеки C(і всіх інших бібліотек, що підключаються). Так само уявіть, що довелося б робити для того, щоб оновити систему, після усунення критичної вразливості в одній з широко задіяних бібліотек.

Тепер небагато практики.

Для ілюстрації скористаємося набором вихідних файлів із попереднього прикладу. У нашу саморобну бібліотеку помістимо код (реалізацію) функцій first() та second().

У Linux прийнята наступна схема іменування файлів бібліотек (хоча дотримується вона не завжди) - ім'я файлу бібліотеки починається з префікса lib, за ним слідує власне ім'я бібліотеки, в кінці розширення. archive ) - для статичної бібліотеки, .so ( shared object ) - для роздільної (динамічної) після розширення через точку перераховуються цифри номера версії (тільки для динамічної бібліотеки). Ім'я, що відповідає бібліотеці заголовного файлу (знову ж зазвичай), складається з імені бібліотеки (без префікса та версії) та розширення .h . Наприклад: libogg.a, libogg.so.0.7.0, ogg.h.

На початку створимо та використовуємо статичну бібліотеку.

Функції first() і second() складуть вміст нашої бібліотеки libhello. Ім'я файлу бібліотеки, відповідно, буде libhello.a. Бібліотеці зіставимо заголовний файл hello.h.

/* hello.h */

void first( void );
void second( void );

Зрозуміло, рядки:

#include "first.h"


#include "second.h"

у файлах main.c, first.c та second.c необхідно замінити на:

#include "hello.h"

Ну а тепер, введемо таку послідовність команд:

$gcc-Wall-c first.c
$gcc-Wall-c second.c
$ ar crs libhello.a first.o second.o
$file libhello.a
libhello.a: current ar archive

Як було зазначено - бібліотека це набір об'єктних файлів. Першими двома командами ми створили ці об'єктні файли.

Далі необхідно об'єктні файли скомпонувати набір. Для цього використовується архіватор ar- утиліта "склеює" кілька файлів на один, в отриманий архів включає інформацію необхідну відновлення (вилучення) кожного індивідуального файла (включаючи його атрибути приналежності, доступу, часу). Будь-якого "стиснення" вмісту архіву або іншого перетворення даних, що зберігаються, при цьому не проводиться.

Опція c arname- створити архів, якщо архів з ім'ям arname не існує він буде створений, інакше файли будуть додані до наявного архіву.

Опція r- задає режим оновлення архіву, якщо в архіві файл із вказаним ім'ям вже існує, він буде видалений, а новий файл дописано до кінця архіву.

Опція s- Додає (оновлює) індекс архіву. У цьому випадку індекс архіву це таблиця, в якій для кожного певного в архівованих файлах символічного імені (ім'я функції або блоку даних) зіставлено відповідне ім'я об'єктного файлу. Індекс архіву необхідний прискорення роботи з бібліотекою - щоб знайти потрібне визначення, відпадає необхідність переглядати таблиці символів всіх файлів архіву, можна одразу перейти до файлу, що містить шукане ім'я. Переглянути індекс архіву можна за допомогою вже знайомої утиліти nmскориставшись її опцією -s(так само буде показано таблиці символів всіх об'єктних файлів архіву):

$nm -s libhello.a
Archive index:
first in first.o
second in second.o

first.o:
00000000 T перша
U puts

second.o:
U puts
00000000 T second

Для створення індексу архіву існує спеціальна утиліта ranlib. Бібліотеку libhello.a можна було створити і так:

$ ar cr libhello.a first.o second.o
$ranlib libhello.a

Втім, бібліотека чудово працюватиме і без індексу архіву.

Тепер скористаємося нашою бібліотекою:

$gcc-Wall-c main.c
$
$./main
First function...
Second function...
Main function...

Працює...

Ну тепер коментарі... З'явилися дві нові опції gcc:

Опція -l name- передається лінковнику, вказує на необхідність підключити до файлу бібліотеку libname . Підключити означає вказати, що такі і такі функції (зовнішні змінні) визначені в бібліотеці. У нашому прикладі бібліотека статична, всі символьні імена будуть посилатися на код, що знаходиться безпосередньо у виконуваному файлі. Зверніть увагу в опції -lІм'я бібліотеки задається як name без приставки lib.

Опція -L /шлях/до/каталогу/з/бібліотеками - передається лінковнику, вказує шлях до каталогу містить бібліотеки, що підключаються. У нашому випадку задана точка . , лінковник спочатку шукатиме бібліотеки в поточному каталозі, потім у каталогах визначених у системі.

Тут потрібно зробити невелике зауваження. Справа в тому, що для низки опцій gccважливий порядок їхнього прямування в командному рядку. Так лінковник шукає код, що відповідає вказаним у таблиці символів файлу іменам у бібліотеках, перерахованих у командному рядку післяімені цього файлу. Вміст бібліотек, перерахованих до імені файлу, лінковник ігнорує:

$gcc-Wall-c main.c
$ gcc-o main-L. -lhello main.o
main.o: In function `main':
main.c:(.text+0xa): undefined reference to `first'
main.c:(.text+0xf): undefined reference to `second"

$ gcc -o main main.o -L. -lhello
$./main
First function...
Second function...
Main function...

Така особливість поведінки gccобумовлена ​​бажанням розробників надати користувачеві можливість по-різному комбінувати файли з бібліотеками, використовувати імена, що перетинають... На мій погляд, якщо можливо, краще цим не морочитися. Загалом підключаються бібліотеки необхідно перераховувати після імені файлу, що посилається на них.

Існує альтернативний спосіб вказівки розташування бібліотек у системі. Залежно від дистрибутива, змінна оточення LD_LIBRARY_PATH або LIBRARY_PATH може зберігати список розділених знаком двокрапки каталогів, у яких лінковник повинен шукати бібліотеки. Як правило, за умовчанням ця змінна взагалі не визначена, але нічого не заважає її створити:

$echo $LD_LIBRARY_PATH

/usr/lib/gcc/i686-pc-linux-gnu/4.4.3/../../../../i686-pc-linux-gnu/bin/ld: cannot find -lhello
collect2: виконання ld завершилося з кодом повернення 1
$ export LIBRARY_PATH=.
$ gcc -o main main.o -lhello
$./main
First function...
Second function...
Main function...

Маніпуляції зі змінними оточення корисні при створенні та налагодженні власних бібліотек, а так само якщо виникає необхідність підключити до додатку якусь нестандартну (застарілу, оновлену, змінену - загалом відмінну від включеної до дистрибутиву) бібліотеку, що розділяється.

Тепер створимо та використовуємо бібліотеку динамічну.

Набір вихідних файлів залишається без змін. Вводимо команди, дивимося що вийшло, читаємо коментарі:

$ gcc -Wall -fPIC -c first.c
$ gcc -Wall -fPIC -c second.c
$ gcc -shared -o libhello.so.2.4.0.5 -Wl,-soname,libhello.so.2 first.o second.o

Що отримали у результаті?

$file libhello.so.2.4.0.5
libhello.so.2.4.0.5: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), динамічно linked, не stripped

Файл libhello.so.2.4.0.5 , це і є наша бібліотека, що розділяється. Як її використати поговоримо трохи нижче.

Тепер коментарі:

Опція -fPIC- вимагає від компілятора при створенні об'єктних файлів породжувати позиційно-незалежний код (PIC - Position Independent Code ), його основна відмінність у способі представлення адрес. Замість вказівки фіксованих (статичних) позицій, всі адреси обчислюються виходячи зі зміщень заданих глобальної таблиці зсувів (Global offset table - GOT ). Формат позиційно-незалежного коду дозволяє підключати модулі, що виконуються, до коду основної програми в момент її завантаження. Відповідно, основне призначення позиційно-незалежного коду - створення динамічних бібліотек, що розділяються.

Опція -shared- Вказує gcc, що в результаті повинен бути зібраний не виконуваний файл, а об'єкт, що розділяється - динамічна бібліотека.

Опція -Wl,-soname,libhello.so.2- задає soname бібліотеки. Про soname докладно поговоримо у наступному абзаці. Зараз обговоримо формат опції. Сея дивна, на перший погляд, конструкція із комами призначена для безпосередньої взаємодії користувача з лінковником. У ході компіляції gccвикликає лінковник автоматично, автоматично ж, на власний розсуд, gccпередає необхідні для успішного завершення завдання опції. Якщо у користувача виникає потреба самому втрутитися в процес лінковки, він може скористатися спеціальною опцією. gcc -Wl, -option, value1, value2 .... Що означає передати лінковнику ( -Wl) опцію -optionз аргументами value1, value2і так далі. У нашому випадку лінковнику було передано опцію -sonameз аргументом libhello.so.2.

Тепер про soname. Під час створення та розповсюдження бібліотек постає проблема сумісності та контролю версій. Для того щоб система, конкретно завантажувач динамічних бібліотек, мали уявлення про те, бібліотека якої версії була використана при компіляції програми і, відповідно, необхідна для його успішного функціонування, був передбачений спеціальний ідентифікатор - soname , що міститься як у файл самої бібліотеки, так і виконуваний файл програми. Ідентифікатор soname це рядок, що включає ім'я бібліотеки з префіксом lib, точку, розширення so, знову точку та оду або дві (розділені точкою) цифри версії бібліотеки - lib name.so. x. y. Тобто soname збігається з ім'ям бібліотеки файлу аж до першої або другої цифри номера версії. Нехай ім'я файлу нашої бібліотеки libhello.so.2.4.0.5 , тоді soname бібліотеки може бути libhello.so.2 . При зміні інтерфейсу бібліотеки її soname необхідно змінювати! Будь-яка модифікація коду, що призводить до несумісності з попередніми релізами, повинна супроводжуватися появою нового soname.

Як це все працює? Нехай для успішного виконання деякого додатка необхідна бібліотека з ім'ям hello , нехай у системі є, при цьому ім'я файлу бібліотеки libhello.so.2.4.0.5 , а прописане в ньому soname бібліотеки libhello.so.2 . На етапі компіляції програми, лінковщик, відповідно до опції -l helloбуде шукати в системі файл з ім'ям libhello.so. У реальній системі libhello.so це символічне посилання на файл libhello.so.2.4.0.5. Отримавши доступ до файлу бібліотеки, лінковник вважає прописане в ньому значення soname і поряд з іншим помістить його у файл програми, що виконується. Коли програма буде запущена, завантажувач динамічних бібліотек отримає запит на підключення бібліотеки з soname, зчитаним з файлу, що виконується, і спробує знайти в системі бібліотеку, ім'я файлу якої збігається з soname. Тобто завантажувач спробує знайти файл libhello.so.2. Якщо система налаштована коректно, в ній має бути символічне посилання libhello.so.2 на файл libhello.so.2.4.0.5 , завантажувач отримає доступ до необхідної бібліотеки і далі не замислюючись (і нічого більше не перевіряючи) підключить її до додатку. Тепер уявімо, що ми перенесли відкомпільований таким чином додаток до іншої системи, де розгорнуто тільки попередня версіябібліотеки з soname libhello.so.1. Спроба запустити програму призведе до помилки, оскільки у цій системі файлу з ім'ям libhello.so.2 немає.

Таким чином, на етапі компіляції лінковнику необхідно надати файл бібліотеки (або символічне посилання на файл бібліотеки) з ім'ям lib name .so , на етапі виконання завантажувачу буде потрібно файл (або символічне посилання) з ім'ям lib name .so. x. y. До чого ім'я lib name .so. x. y має збігатися з рядком soname використаної бібліотеки.

У бінарних дистрибутивах, як правило, файл бібліотеки libhello.so.2.4.0.5 і посилання на нього libhello.so.2 будуть поміщені в пакет libhello , а необхідне тільки для компіляції посилання libhello.so разом з заголовним файлом бібліотеки hello.h буде упакована в пакет libhello-devel (у devel пакеті виявиться і файл статичної версії бібліотеки libhello.a, статична бібліотека може бути використана також на етапі компіляції). Під час розпакування пакета всі перелічені файли та посилання (крім hello.h) виявляться в одному каталозі.

Переконаємося, що заданий рядок soname дійсно прописаний у нашій бібліотеці. Скористаємося мега утилітою objdumpз опцією -p :

$ objdump -p libhello.so.2.4.0.5 | grep SONAME
SONAME libhello.so.2


Утиліта objdump- потужний інструмент, що дозволяє отримати вичерпну інформацію про внутрішній зміст (і пристрій) об'єктного або файлу, що виконується. У сторінці утиліти сказано, що objdumpперш за все буде корисним програмістам, які створюють засоби налагодження та компіляції, а не просто пишуть якісь прикладні програми:) Зокрема з опцією -dце дизассемблер. Ми скористалися опцією -p- Вивести різну метаінформацію про об'єктний файл.

У наведеному прикладі створення бібліотеки ми невідступно дотримувалися принципів роздільної компіляції. Зрозуміло скомпілювати бібліотеку можна було б і ось так, одним викликом gcc:

$ gcc -shared -Wall -fPIC -o libhello.so.2.4.0.5 -Wl,-soname,libhello.so.2 first.c second.c

Тепер спробуємо скористатися бібліотекою, що вийшла:

$gcc-Wall-c main.c
$
/usr/bin/ld: cannot find -lhello
collect2: ld returned 1 exit status

Лінковщик лається. Згадуємо, що було сказано вище про символічні посилання. Створюємо libhello.so і повторюємо спробу:

$ ln -s libhello.so.2.4.0.5 libhello.so
$ gcc -o main main.o -L. -lhello -Wl,-rpath,.

Тепер усі задоволені. Запускаємо створений бінарник:

Помилка... Лається завантажувач, який не може знайти бібліотеку libhello.so.2 . Переконаємося, що у файлі, що виконується, дійсно прописане посилання на libhello.so.2 :

$objdump-p main | grep NEEDED
NEEDED libhello.so.2
NEEDED libc.so.6

$ ln -s libhello.so.2.4.0.5 libhello.so.2
$./main
First function...
Second function...
Main function...

Запрацювало... Тепер коментарі щодо нових опцій gcc.

Опція -Wl, -rpath,.- вже знайома конструкція, передати лінковнику опцію -rpathз аргументом . . За допомогою -rpathу виконуваний файл програми можна прописати додаткові шляхи якими завантажувач бібліотек буде здійснювати пошук бібліотечних файлів. У нашому випадку прописаний шлях . - пошук файлів бібліотек розпочинатиметься з поточного каталогу.

$objdump-p main | grep RPATH
RPATH.

Завдяки зазначеній опції при запуску програми відпала необхідність змінювати змінні оточення. Зрозуміло, якщо перенести програму в інший каталог і спробувати запустити, файл бібліотеки буде не знайдений і завантажувач видасть повідомлення про помилку:

$ mv main ..
$../main
First function...
Second function...
Main function...

Дізнатися які бібліотеки, що розділяються, необхідні додатку можна і за допомогою утиліти. ldd:

$ ldd main
linux-vdso.so.1 => (0x00007fffaddff000)
libhello.so.2 => ./libhello.so.2 (0x00007f9689001000)
libc.so.6 => /lib/libc.so.6 (0x00007f9688c62000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9689205000)

У висновку lddдля кожної необхідної бібліотеки вказується її soname та повний шлях до файлу бібліотеки, визначений відповідно до налаштувань системи.

Зараз саме час поговорити про те, де в системі належить розміщувати файли бібліотек, де завантажувач намагається їх знайти і як цим процесом керувати.

Відповідно до угод FHS (Filesystem Hierarchy Standard)у системі мають бути два (як мінімум) каталоги для зберігання файлів бібліотек:

/lib - тут зібрані основні бібліотеки дистрибутива, необхідні роботи програм з /bin і /sbin ;

/usr/lib - тут зберігаються бібліотеки необхідні прикладним програмам з /usr/bin та /usr/sbin;

Заголовні файли, що відповідають бібліотекам, повинні знаходитися в каталозі /usr/include .

Завантажувач за замовчуванням шукатиме файли бібліотек у цих каталогах.

Крім перелічених вище, в системі повинен бути каталог /usr/local/lib - тут повинні знаходитися бібліотеки, розгорнуті користувачем самостійно, минаючи систему управління пакетами (що не входять до складу дистрибутива). Наприклад, у цьому каталозі за замовчуванням виявляться бібліотеки скомпільовані з вихідних кодів (програми встановлені з вихідних джерел будуть розміщені в /usr/local/bin і /usr/local/sbin , зрозуміло йдеться про бінарних дистрибутивів). Заголовні файли бібліотек у цьому випадку будуть розміщені в /usr/local/include .

У ряді дистрибутивів (у Ubuntu) завантажувач не налаштований переглядати каталог /usr/local/lib відповідно, якщо користувач встановить бібліотеку з вихідних джерел, система її не побачить. Це авторами дистрибутива зроблено спеціально, щоб привчити користувача встановлювати програмне забезпеченнялише через систему керування пакетами. Як діяти в даному випадку буде розказано нижче.

Для спрощення та прискорення процесу пошуку файлів бібліотек завантажувач не переглядає при кожному зверненні вказані вище каталоги, а користується базою даних, що зберігається у файлі /etc/ld.so.cache (кешем бібліотек). Тут зібрана інформація про те, де в системі знаходиться відповідний файл файл бібліотеки. Завантажувач, отримавши список необхідних конкретному додатку бібліотек (список soname бібліотек, заданих у файлі програми), за допомогою /etc/ld.so.cache визначає шлях до файлу кожної необхідної бібліотеки і завантажує її в пам'ять. Додатково, завантажувач може переглянути каталоги, перелічені в системних змінних LD_LIBRARY_PATH , LIBRARY_PATH та в полі RPATH виконуваного файлу (дивися вище).

Для керування та підтримки в актуальному стані кешу бібліотек використовується утиліта ldconfig. Якщо запустити ldconfigбез будь-яких опцій, програма перегляне каталоги, задані в командному рядку, довірені каталоги /lib і /usr/lib , каталоги перелічені у файлі /etc/ld.so.conf . Для кожного файлу бібліотеки, що опинився у вказаних каталогах, буде раховано soname, створене на основі soname символічне посилання, оновлено інформацію в /etc/ld.so.cache .

Переконаємося у сказаному:

$ls
hello.h libhello.so libhello.so.2.4.0.5 main.c
$
$ sudo ldconfig /повний/шлях/до/катаогу/c/прикладом
$ls
hello.h libhello.so libhello.so.2 libhello.so.2.4.0.5 main main.c
$./main
First function...
Second function...
Main function...

Першим викликом ldconfigми внесли в кеш нашу бібліотеку, виключили другим викликом. Зауважте, що при компіляції main була опущена опція -Wl,-rpath,., в результаті завантажувач проводив пошук необхідних бібліотек лише у кеші.

Тепер має бути зрозуміло як вчинити, якщо після встановлення бібліотеки з вихідних джерел система її не бачить. Насамперед необхідно внести до файлу /etc/ld.so.conf повний шлях до каталогу з файлами бібліотеки (за замовчуванням /usr/local/lib ). Формат /etc/ld.so.conf - файл містить список розділених двокрапкою, пробілом, табуляцією або символом нового рядка каталогів, в яких проводиться пошук бібліотек. Після чого викликати ldconfigбез будь-яких опцій, але з правами суперкористувача. Все має заробити.

Ну і в кінці поговоримо про те, як уживаються разом статичні та динамічні версії бібліотек. У чому саме питання? Вище, коли обговорювалися прийняті імена та розташування файлів бібліотек було сказано, що файли статичної та динамічної версій бібліотеки зберігаються в тому самому каталозі. Як же gccдізнається який тип бібліотеки ми хочемо використати? За замовчуванням перевага надається динамічній бібліотеці. Якщо лінковник знаходить файл динамічної бібліотеки, він не замислюючись чіпляє його до файлу програми, що виконується:

$ls
hello.h libhello.a libhello.so libhello.so.2 libhello.so.2.4.0.5 main.c
$gcc-Wall-c main.c
$ gcc -o main main.o -L. -lhello -Wl,-rpath,.
$ ldd main
linux-vdso.so.1 => (0x00007fffe1bb0000)
libhello.so.2 => ./libhello.so.2 (0x00007fd50370b000)
libc.so.6 => /lib/libc.so.6 (0x00007fd50336c000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd50390f000)
$ du -h main
12K main

Зверніть увагу на розмір файлу програми, що виконується. Він мінімально можливий. Усі бібліотеки лінуються динамічно.

Існує опція gcc-static- вказівку лінковнику використовувати лише статичні версії всіх необхідних додатку бібліотек:

$ gcc-static-o main main.o-L. -lhello
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), статичне linked, for GNU/Linux 2.6.15, не stripped
$ ldd main
не є динамічним виконуваним файлом
$ du -h main
728K main

Розмір файлу, що виконується в 60 разів більше, ніж у попередньому прикладі - у файл включені стандартні бібліотеки мови C. Тепер наша програма можна сміливо переносити з каталогу в каталог і навіть на інші машини, код бібліотеки hello всередині файлу, програма повністю автономна.

Як же бути, якщо необхідно здійснити статичне лінкування лише частини використаних бібліотек? Можливий варіант рішення - зробити ім'я статичної версії бібліотеки відмінним від імені розділеної, а при компіляції програми вказувати яку версію ми хочемо використовувати цього разу:

$ mv libhello.a libhello_s.a
$ gcc -o main main.o -L. -lhello_s
$ ldd main
linux-vdso.so.1 => (0x00007fff021f5000)
libc.so.6 => /lib/libc.so.6 (0x00007fd0d0803000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd0d0ba4000)
$ du -h main
12K main

Оскільки розмір коду бібліотеки libhello незначний,

$ du -h libhello_s.a
4,0K libhello.a

розмір файлу, що виконується, практично не відрізняється від розміру файлу створеного з використанням динамічної лінківки.

Ну ось мабуть і все. Дуже дякую всім, хто закінчив читання на цьому місці.

Тепер, коли ви дізналися про стандарт С, давайте розглянемо опції, які пропонує компілятор gcc для гарантії відповідності стандарту мови С, на якому ви пишете. Є три способи, що дозволяють переконатися в тому, що ваш код на С відповідає стандартам і не містить вад: опції, що контролюють версію стандарту, відповідності з якою ви маєте намір домагатися, визначення, що контролюють заголовні файли, та опції попереджень, що ініціюють суворішу перевірку програмного коду .

gcc має величезний набір опцій, і тут ми розглянемо лише ті з них, які вважаємо найбільш важливими. Повний перелік опцій можна знайти на сторінках інтерактивного довідкового посібника gcc. Ми також коротко обговоримо деякі опції директиви #define, які можна застосовувати; зазвичай їх слід задавати у вихідному програмному коді перед будь-якими рядками з директивою #include або визначати в командному рядку gcc. Вас може здивувати таку різноманітність опцій для вибору застосованого стандарту замість простого прапора, що змушує використовувати сучасний стандарт. Причина полягає в тому, що багато більш старих програм покладається на історично сформовану поведінку компілятора і знадобилася б значна робота щодо їх оновлення відповідно до останніх стандартів. Рідко, якщо взагалі коли-небудь, вам захочеться оновити компілятор для того, щоб він почав переривати програмний код, що працює. У міру зміни стандартів важливо мати можливість працювати всупереч певному стандарту, навіть якщо це і не найсвіжіша версія стандарту.

Навіть якщо ви пишете маленьку програму для особистого застосування, коли відповідність стандартам, можливо, не така вже й важлива, часто має сенс включити додаткові попередження gcc, щоб змусити компілятор шукати помилки у вашому коді ще до виконання програми. Це завжди ефективніше, ніж виконувати кроки код у відладчику і дивуватися з приводу можливого місця виниклої проблеми. Компілятор має багато опцій, які не обмежуються простою перевіркою на відповідність стандартам, таких, як здатність виявляти код, який задовольняє стандарту, але, можливо, має сумнівну семантику. Наприклад, у програмі може бути такий порядок виконання, який дозволяє звертатися до змінної до її ініціалізації.

Якщо вам потрібно написати програму для колективного використання, при вибраних ступеня відповідності стандарту та типах попереджень компілятора, які ви вважаєте достатніми, дуже важливо витратити трохи більше зусиль і досягти компіляції вашого коду без будь-яких попереджень взагалі. Якщо ви допустите наявність деяких попереджень і звикніть їх ігнорувати, одного чудового дня може з'явитися більш серйозне попередження, яке ви ризикуєте пропустити. Якщо ваш програмний код завжди компілюється без попереджувальних повідомлень, нове попередження неминуче приверне вашу увагу. Компіляція програмного коду без попереджень – корисна звичка, яку варто взяти на озброєння.

Опції компілятора для відстеження стандартів

Ansi - це найважливіша опція, що стосується стандартів та змушує компілятор діяти відповідно до стандарту мови ISO C90. Вона відключає деякі розширення gcc, не сумісні зі стандартом, відключає у програмах мовою З коментарі у стилі С++ (//) і включає обробку триграфів (трисимвольних послідовностей) ANSI. Крім того, вона містить макрос __ STRICT_ANSI__ , який відключає деякі розширення в файлах заголовків, не сумісні зі стандартом. У наступних версіях компілятора стандарт може змінитися.

Std= - ця опція забезпечує більш тонкий контроль стандарту, що використовується, надаючи параметр, що точно задає необхідний стандарт. Далі наведено основні можливі варіанти:

С89 – підтримувати стандарт C89;

Iso9899:1999 - підтримувати останню версіюстандарту ISO, C90;

Gnu89 - підтримувати стандарт C89, але дозволити деякі розширення GNU та деякі функціональні можливості C99. У версії 4.2 gcc цей варіант застосовується за умовчанням.

Опції для відстеження стандарту у директивах define

Існують константи (#defines), які можуть бути задані опціями в командному рядку або вигляді визначень у вихідному тексті програми. Ми зазвичай вважаємо, що для них використовується командний рядок компілятора.

STRICT_ANSI__ - змушує застосовувати стандарт ISO. Визначається, коли в командному рядку компілятора встановлено опцію -ansi .

POSIX_C_SOURCE=2 - активізує функціональні можливості, визначені стандартами IEEE Std 1003.1 та 1003.2. Ми повернемося до цих стандартів трохи пізніше у цьому розділі.

BSD_SOURCE – включає функціональні можливості систем BSD. Якщо вони конфліктують з визначеннями POSIX, визначення BSD мають більш високий пріоритет.

GNU_SOURCE - дозволяє широкий діапазон властивостей та функцій, включаючи розширення GNU. Якщо ці визначення конфліктують із визначеннями POSIX, у останніх вищий пріоритет.

Опції компілятора для попередження

Ці опції передаються компілятор з командного рядка. І знову ми перерахуємо лише основні, повний списокможна знайти в інтерактивному довідковому посібнику gcc.

Pedantic - ця найбільш потужна опція перевірки чистоти, програмного коду на мові С. Крім включення опції перевірки на відповідність стандарту С, вона відключає деякі традиційні конструкції мови С, заборонені стандартом, і робить неприпустимими всі розширення GNU стосовно стандарту. Цю опцію слід застосовувати, щоб домогтися максимальної переносимості вашого коду на С. Недолік її в тому, що компілятор сильно стурбований чистотою вашого програмного коду, і часом доводиться поламати голову для того, щоб розправитися з кількома попередженнями, що залишилися.

Wformat - перевіряє коректність типів аргументів функцій сімейства printf.

Wparentheses - перевіряє наявність дужок, навіть там, де вони не потрібні. Ця опція дуже корисна для перевірки того, що складні структури ініціалізовані так, як задумано.

Wswitch-default - перевіряє наявність варіанта default в операторах switch, що зазвичай вважається добрим стилем програмування.

Wunused - перевіряє різноманітні випадки, наприклад, статичні функції оголошені, але не описані параметри, що не використовуються, відкинуті результати.

Wall - включає більшість типів попереджень gcc, у тому числі всі попередні опції - W (не охоплює лише -pedantic). З її допомогою легко досягти чистоти програмного коду.

Примітка

Існує ще безліч додаткових опцій попереджень, всі подробиці див. на Web-сторінках gcc. В основному ми рекомендуємо застосовувати-Wall; це вдалий компроміс між перевіркою, що забезпечує програмний код високої якості, та необхідністю виведення компілятором маси тривіальних попереджень, які стає важко звести до нуля.

Для правильного використання gcc , стандартного компілятора для Linux, необхідно вивчити опції командного рядка. Крім того, gcc розширює мову С. Навіть якщо ви маєте намір писати вихідний код, дотримуючись ANSI-стандарту цієї мови, деякі розширення gcc просто необхідно знати для розуміння заголовних файлів Linux.

Більшість опцій командного рядка такі ж, як застосовувані в компіляторах С. Для деяких опцій жодних стандартів не передбачено. У цьому розділі ми охопимо найважливіші опції, що використовуються у повсякденному програмуванні.

Прагнення дотриматися ISO-стандарт С дуже корисно, але у зв'язку з тим, що є низькорівневою мовою, зустрічаються ситуації, коли стандартні засоби недостатньо виразні. Існують дві області, в яких широко застосовуються розширення gcc: взаємодія з асемблерним кодом (ці питання розкриваються за адресою http://www.delorie.com/djgpp/doc/brennan/) та складання бібліотек, що спільно використовуються (див. розділ 8). Оскільки заголовні файли є частиною бібліотек, що спільно використовуються, деякі розширення виявляються також у системних заголовкових файлах.

Звичайно, існує ще безліч розширень, корисних у будь-якому іншому вигляді програмування, які можуть дуже допомогти при кодуванні. Додаткову інформаціюза цими розширеннями можна знайти у документації gcc у форматі Texinfo.

5.1. Опції gcc

gcc приймає багато командних опцій. На щастя, набір опцій, про які дійсно потрібно мати уявлення, не такий великий, і в цьому розділі ми його розглянемо.

Більшість опцій збігаються або подібні до опцій інших компіляторів, gcc включає в себе величезну документацію за своїми опціями, доступну через info gcc (man gcc також видає цю інформацію, проте man-сторінки не оновлюються настільки часто, як документація у форматі Texinfo).

-о ім'я_файлу Вказує назву вихідного файлу. Зазвичай у цьому немає потреби, якщо здійснюється компіляція в об'єктний файл, тобто за умовчанням відбувається підстановка имя_файла.с на имя_файла.о. Однак якщо ви створюєте файл, що виконується, за умовчанням (з історичних причин) він створюється під ім'ям а.out . Це також корисно, якщо потрібно помістити вихідний файл в інший каталог.
Компілює без компонування вихідний файл, вказаний для командного рядка. У результаті кожного вихідного файлу створюється об'єктний файл. При використанні make компілятор gcc зазвичай викликається кожного об'єктного файла; таким чином, у разі виникнення помилки легше виявити, який файл не зміг скомпілюватися. Однак, якщо ви вручну набираєте команди, часто в одному виклику gcc вказується безліч файлів. У разі, якщо при заданні множини файлів у командному рядку може виникнути неоднозначність, краще вказати лише один файл. Наприклад, замість gcc -з -о а.о а.с b.с має сенс застосувати gcc -з -o a.o b.c.
-D foo Визначає препроцесорні макроси командного рядка. Можливо, потрібно буде скасувати символи, що трактуються оболонкою як спеціальні. Наприклад, при визначенні рядка слід уникати вживання символів, що обмежують рядки. оболонка розглядає прогалини особливим чином.
-I каталог Додає каталог до списку каталогів, в яких проводиться пошук файлів, що включаються.
-L каталог Додає каталог до списку каталогів, в яких здійснюється пошук бібліотек, gcc віддаватиме перевагу спільно використовуваним бібліотекам, а не статичним, якщо тільки не встановлено зворотне.
-l foo Виконує компонування з бібліотекою lib foo. Якщо не вказано зворотне, gcc віддає перевагу компонуванням із спільно використовуваними бібліотеками (lib foo .so), а не статичними (lib foo .a). Компонувальник здійснює пошук функцій у всіх перерахованих бібліотеках у тому порядку, в якому вони перераховані. Пошук завершується тоді, коли будуть знайдені всі потрібні функції.
-static Виконує компонування з лише статичними бібліотеками. розділ 8.
-g, -ggdb Включає налагоджувальну інформацію. Опція -g змушує gcc увімкнути стандартну налагоджувальну інформацію. Опція -ggdb вказує на необхідність включення величезної кількості інформації, яку може зрозуміти лише відладчик gdb.
Якщо дисковий простір обмежений або ви хочете пожертвувати деякою функціональністю для швидкості компонування, слід використовувати -g . В цьому випадку, можливо, доведеться скористатися іншим налагоджувачем, а не gdb. Для максимально повного налагодження необхідно вказувати -ggdb. У цьому випадку gcc підготує максимально докладну інформацію для gdb. Слід зазначити, що на відміну більшості компіляторів, gcc поміщає деяку налагоджувальну інформацію в оптимізований код. Однак трасування у налагоджувачі оптимізованого коду може бути пов'язане зі складнощами, оскільки під час виконання можуть відбуватися стрибки та пропуски фрагментів коду, які, як очікувалося, мали виконуватися. Тим не менш, при цьому можна отримати гарне уявлення про те, як компілятори, що оптимізують, змінюють спосіб виконання коду.
-O , -O n Примушує gcc оптимізувати код. За умовчанням gcc виконує невеликий обсяг оптимізації; при вказівці числа (n) здійснюється оптимізація певному рівні. Найбільш поширений рівень оптимізації – 2; в даний час у стандартній версії gcc найбільш високим рівнемоптимізації є 3. Ми рекомендуємо використовувати -O2 або -O3; -O3 може збільшити розмір програми, так що якщо це має значення, спробуйте обидва варіанти. Якщо для вашої програми важлива пам'ять і дисковий простір, можна використовувати опцію -Os , яка робить розмір коду мінімальним за рахунок збільшення часу виконання. gcc включає вбудовані функції лише тоді, коли застосовується хоча б мінімальна оптимізація (-O).
-ansi Підтримка в програмах мовою З усіх стандартів ANSI (X3.159-1989) або їх еквівалент ISO (ISO/IEC 9899:1990) (звичайне званого С89 або рідше С90). Слід зазначити, що це забезпечує повну відповідність стандарту ANSI/ISO.
Опція -ansi відключає розширення gcc, які зазвичай конфліктують із стандартами ANSI/ISO. (Внаслідок того, що ці розширення підтримуються багатьма іншими компіляторами С, на практиці це не є проблемою.) Це також визначає макрос __STRICT_ANSI__ (як описано далі в цій книзі), який заголовні файли використовують для підтримки середовища, що відповідає ANSI/ISO.
-pedantic Виводить усі попередження та повідомлення про помилки, потрібні для ANSI/ISO-стандарту мови С. Це не забезпечує повну відповідність стандарту ANSI/ISO.
-Wall Включає генерацію всіх попереджень gcc, що є корисним. Але таким чином не включаються опції, які можуть стати в нагоді у специфічних випадках. Аналогічний рівень деталізації буде встановлений і для програми синтаксичного контролю lint щодо вашого вихідного коду, gcc дозволяє вручну вмикати та вимикати кожне попередження компілятора. У посібнику gcc детально описані всі попередження.
5.2. Заголовні файли
5.2.1. long long

Тип long long вказує на те, що блок пам'яті принаймні такий же великий, як long . На Intel i86 та інших 32-розрядних платформах long займає 32 біти, а long long – 64 біти. На 64-розрядних платформах покажчики та long long займають 64 біти, a long може займати 32 або 64 біти залежно від платформи. Тип long long підтримується у стандарті С99 (ISO/IEC 9899:1999) і є давнім розширенням, яке забезпечується gcc .

5.2.2. Вбудовані функції

У деяких частинах заголовних файлів Linux (зокрема тих, що є специфічними для конкретної системи) вбудовані функції використовуються дуже широко. Вони так само швидкі, як і макроси (немає витрат на виклики функції) і забезпечують всі види перевірки, які доступні при нормальному виклику функції. Код, що викликає вбудовані функції, повинен компілюватися принаймні з включеною мінімальною оптимізацією (-O).

5.2.3. Альтернативні розширені ключові слова

У gcc у кожного розширеного ключового слова (ключові слова, які не описані стандартом ANSI/ISO) є дві версії: саме ключове словота ключове слово, оточене з двох сторін двома символами підкреслення. Коли компілятор застосовується у стандартному режимі (зазвичай тоді, коли задіяна опція -ansi), звичайні розширені ключові слова не розпізнаються. Так, наприклад, ключове слово attribute в заголовку має бути записане як __attribute__ .

5.2.4. Атрибути

Розширене ключове слово attribute використовується для передачі gcc більшого обсягу інформації про функцію, змінну або оголошений тип, ніж це дозволяє код С, що відповідає стандарту ANSI/ISO. Наприклад, атрибут aligned дає вказівку gcc у тому, як саме вирівнювати змінну чи тип; атрибут packed вказує на те, що заповнення не використовуватиметься; noreturn визначає те, що повернення з функції ніколи не відбудеться, що дозволяє gcc краще оптимізуватися та уникати фіктивних попереджень.

Атрибути функції оголошуються шляхом їх додавання до оголошення функції, наприклад:

void die_die_die(int, char*) __attribute__ ((__noreturn__));

Оголошення атрибута розміщується між дужками та точкою з комою і містить ключове слово attribute , за яким слідують атрибути у подвійних круглих дужках. Якщо атрибутів багато, слід використовувати список, розділений комами.

int printm(char*, ...)

Attribute__((const,

format(printf, 1, 2)));

У цьому прикладі видно, що printm не розглядає жодних значень, крім зазначених, і не має побічних ефектів, що відносяться до генерації коду (const), printm вказує на те, що gcc повинен перевіряти аргументи функції так само, як і printf() . Перший аргумент є форматуючим рядком, а другий - першим параметром заміни (format).

Деякі атрибути будуть розглядатися в міру подальшого викладення матеріалу (наприклад, під час опису збірки бібліотек, що спільно використовуються в розділі 8). Вичерпну інформацію щодо атрибутів можна знайти в документації gcc у форматі Texinfo.

Іноді ви можете застати себе на тому, що переглядаєте заголовні файли Linux. Швидше за все, ви знайдете ряд конструкцій, які не сумісні зі стандартом ANSI/ISO. Деякі з них варті того, щоб у них розібратися. Всі конструкції, що розглядаються в цій книзі, більш детально викладені в документації gcc.

Іноді ви можете застати себе на тому, що переглядаєте заголовні файли Linux. Швидше за все, ви знайдете ряд конструкцій, які не сумісні зі стандартом ANSI/ISO. Деякі з них варті того, щоб у них розібратися. Всі конструкції, що розглядаються в цій книзі, більш детально викладені в документації gcc.

gcc (GNU C Compiler) - набір утиліт для компіляції, асемблювання та компонування. Їх метою є створення готового до запуску файлу у форматі, що розуміється вашою ОС. Для Linux цим форматом є ELF (Executable and Linking Format) на x86 (32- і 64-бітних). Але чи знаєте ви, що можуть зробити деякі параметри gcc? Якщо ви шукаєте способи оптимізації одержуваного бінарного файлу, підготовки сесії налагодження або просто спостерігати за діями gcc для перетворення вашого вихідного коду на виконуваний файл, то знайомство з цими параметрами обов'язкове. Тож читайте.

Нагадаю, що gcc робить кілька кроків, а не лише один. Ось невелике пояснення їхнього сенсу:

    Препроцесування: Створення коду, який більше не містить директив. Речі на кшталт "#if" не можуть бути зрозумілі компілятором, тому повинні бути переведені в реальний код. Також на цій стадії розгортаються макроси, роблячи підсумковий код більше ніж оригінальний.

    Компіляція: Береться оброблений код, проводяться лексичний та синтаксичний аналізи, та генерується асемблерний код. Протягом цієї фази gcc видає повідомлення про помилки або попередження у випадку, якщо аналізатор при парсингу вашого коду знаходить там якісь помилки. Якщо запитується оптимізація, gcc продовжить аналізувати ваш код у пошуках покращень та маніпулювати з ними надалі. Ця робота відбувається в багатопрохідному стилі, що показує те, що іноді потрібно більше одного проходу за кодом для оптимізації.

    Асемблювання: Приймаються асемблерні мнемоніки та виробляються об'єктні коди, що містять коди команд. Часто не розуміють те, що на стадії компіляції не виробляються коди команд, це робиться на стадії асемблювання. В результаті виходять один або більше об'єктних файлів, що містять коди команд, які є дійсно машинозалежними.

    Компонування: Трансформує об'єктні файли на підсумкові виконувані. Лише кодів операції недостатньо для того, щоб операційна системарозпізнала та виконала їх. Вони мають бути вбудовані у більш повну форму. Ця форма, відома як бінарний формат, показує, як ОС завантажує бінарний файл, компонує переміщення та робить іншу необхідну роботу. ELF є стандартним форматом для Linux на x86.

    Параметри gcc описані тут, прямо і опосередковано торкаючись всіх чотирьох стадій, тому для ясності, ця стаття побудована таким чином:

    Параметри, що стосуються оптимізації

    Параметри, які стосуються виклику функцій

    Параметри, які стосуються налагодження

    Параметри, що стосуються препроцесування

    Насамперед, давайте ознайомимося з допоміжними інструментами, які допоможуть нам проникати у підсумковий код:

    Колекція утиліт ELF, яка включає такі програми, як objdump і readelf. Вони парять для нас інформацію про ELF.

    Ступінь помилки = ізольовані помилкові гілки / ізольовані гілки

    Тепер обчислимо ступінь помилки кожного бінарного файла. Для non-optimized вийшло 0,5117%, у той час як optimizedO2 отримав 0,4323% - у нашому випадку, вигода дуже мала. Фактична вигода може відрізнятися реальних випадків, оскільки gcc сам собою може багато зробити без зовнішніх вказівок. Будь ласка, прочитайте про __builtin_expect() у документації gcc для детальної інформації.