Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 10

COMPLIES WITH GCC AND GNU MAKE

I. GCC

gcc (GNU Compiler Collection) là tập hợp các chương trình dịch dùng để biên
dịch các ngôn ngữ khác nhau. gcc là chương trình dịch chính thức của hệ thống GNU
bao gồm hệ điều hành giống-UNIX, Linux, Mac OS và một số hệ điều hành khác. Quá
trình biên dịch  sẽ phải đi qua một chuỗi các bước để tạo ra file thực thi cuối cùng. Các
bước trung gian đó là kết quả của các tool khác nhau được gọi bên trong gcc để hoàn
thành quá trình dịch source code.
Toàn bộ quá trình dịch bằng GCC được chia ra làm các bước sau:
 Preprocessing
 Compilation
 Assembler
 Linking
Bài viết này sẽ sử dụng file hello.c sau làm ví dụ:

#include <stdio.h>
#define STRING "Hello World"

int main (void)


{
printf ("My First program - %s\n",STRING);
return 0;
}

1. Preprocessing (tiền xử lý)


Về cơ bản bộ tiền xử lý C có 3 nhiệm vụ chính:
 Text Substitution: thay thế text
 Stripping of Comments: loại bỏ comment
 File Inclusion: nối file (mở rộng file)
Việc thay thế text và nối file đươc yêu cầu trong code bằng cách sử dụng Preprocessor
Directives, đó chính là các dòng code bắt đầu với ký tự “#”.
Thường thì ở bước này các file header và các macro được defined sẽ được mở rộng và
gộp vào trong source code tạo thành một file tạm, thường gọi là đơn vị dịch
( translation unit hay compilation unit). Bộ tiền xử lý C, hay còn gọi là cpp, là một đơn
vị vi xử lý được sử dụng vởi trình biên dịch C để biến đổi chương trình C trước khi
dịch. Để thực hiện tiền xử lý ta thường dùng dòng lệnh:
[bash]$ cpp hello.c > hello.i

Kết quả ta được output là file hello.i chứa source code đã được mở rộng bao gồm các
macro. Theo quy ước file mở rộng này sẽ có phần đuôi là .i đối với C và .ii đối với C+
+.
Note: Mặc định các file output của quá trình tiền xử lý này sẽ không được lưu vào đĩa
cứng trừ khi ta dùng gcc với option -save-temps.
Chúng ta có thể trực tiếp thực hiện quá trình tiền xử lý với gcc bằng cách dùng cờ “-
E”.

[bash]$ gcc -E hello.c -o hello.i

File output sẽ có dạng như sau. Vì file studio.h quá lớn nên mình xin phép bỏ qua 

# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3
# 1 "/usr/include/_ansi.h" 1 3
# 1 "/usr/include/sys/config.h" 1 3
# 14 "/usr/include/sys/config.h" 3
# 25 "/usr/include/sys/config.h" 3
# 44 "/usr/include/sys/config.h" 3

# 40 "/usr/include/stdio.h" 2 3
# 1 "/usr/include/sys/reent.h" 1 3

int main(void){

printf ("My First Program - %s\n", "HELLO WORLD" );


return 0;
}

Vì chương trình yêu cầu file stdio.h, từ đó kéo theo yêu cầu thêm các file khác nữa. Vì
vậy, bộ tiền xử lý tạo ra các ghi chú về file và dòng trong file nơi thực hiện request file
để thực hiện các bước tiếp theo. Do vậy, các dòng,

# 40 "/usr/include/stdio.h" 2 3
# 1 "/usr/include/sys/reent.h" 1 3

chỉ ra rằng file reent.h được request ở dòng 40 trong file stdio.h. Bộ tiền xử lý tạo ra số


dòng và gắn nhãn file name mà nó có thể dùng tới ở các bước tiếp theo. Nhờ đó nó có
thể biết được lỗi xảy ra ở đâu trong quá trình biên dịch.
2. Compilation
Bước tiếp theo là quá trình biên dịch từ source code đã thực hiện tiền xử lý ra file
source code assembly. Với mỗi một bộ vi xử lý, mỗi kiến trúc vi xử lý khác nhau thì
file source code  sẽ được dịch ra ngôn ngữ assembly tương ứng.
Dùng gcc với cờ -S để convert file source code đã tiền xử lý ra ngôn ngữ assembly mà
không tạo ra file object :

[bash]$ gcc -Wall -S hello.i -o hello.s

Kết quả ta có file hello.s dưới dạng ngôn ngữ assembly.


3. Assembler
Chúng ta đã được dạy là máy tình thì chỉ hiểu được ngôn ngữ máy. Vì thế chúng ta
phải dịch file hello.s ra ngôn ngữ máy để máy tính có thể hiểu và thực thi. Mặc dù quá
trình dịch từ một ngôn ngữ assembly ra ngôn ngữ máy là quá trình mapping 1-1,
nhưng nếu thực hiện bằng tay thì đây là công việc nhàm chán và rất dễ gây ra lỗi.
NOTE: ASSEMBLER là một trong những công cụ phần mềm đầu tiên được phát minh
sau sự ra đời của máy tính điện tử.
Nếu có một lời gọi hàm nào từ trong code assembly ra bên ngoài file, Assembler sẽ để
địa chỉ lời gọi đó là chưa xác định (undefined), và sẽ được điền vào ở bước tiếp theo,
Linker.
Assembler trong gcc có thể được gọi như dưới đây:

[bash]$ as hello.s -o hello.o

Với gcc, file output được xác định với option -o. File hello.o chính là file mã máy
của hello.c.
hoặc dùng option “-c” của gcc để convert:

[bash]$ gcc -c hello.c

4. Linker
Bước cuối cùng của quá trình biên dịch là tạo ra một file thực thi duy nhất bằng cách
link các file object. Một file object và file thực thi có nhiều định dạng như ELF
(Executable and Linking Format) và COFF (Common Object-File Format). Ví dụ,
ELF được sử dụng trên các máy Linux, COFF được sử dụng trên máy Windows.
Trong thực tế, một file thực thi gọi đến nhiều hàm bên ngoài từ hệ thống và các thư
viện C. Linker sẽ xử lý tất cả các thành phần phụ thuộc và cung cấp địa chỉ của các
hàm được gọi.
Linker cũng thực hiện một số nhiệm vụ khác. Nó kết hợp chương trình của chúng ta
với một số tác vụ chuẩn cần thiết giúp chương trình có thể chạy. Ví dụ như thiết lập
môi trường chạy, truyền các tham số dòng lệnh và các biến môi trường. Ngoài ra, còn
cần có code chạy ở cuối chương trình, nơi kết quả được retrun. Do đó khối lượng code
là không hề nhỏ.
Linker trong gcc có thể được gọi như dưới đây:

[bash]$ gcc hello.o

Nó sẽ link file object hello.o với các thư viện chuẩn của C và tạo ra file thực thi a.out.
II. GNU MAKE
1. Makefile là gì?
 Makefile là một file dạng script chứa các thông tin:
    - Cấu trúc project (file, sự phụ thuộc)
    - Các lệnh để tạo file
 Lệnh make sẽ đọc nội dung Makefile, hiểu kiến trúc của project và thực
thi các lệnh
2. Cấu trúc project
 Cấu trúc và sự phụ thuộc của project có thể được biểu diễn bằng một
DAG (Directed Acyclic Graph)
 Thí dụ:
    - Chương trình chứa 3 file: main.c, sum.c, sum.h
    - File sum.h được dùng bởi cả 2 file main.c và sum.c
    - File thực thi là sum
sum.h
#ifndef SUM_H_
#define SUM_H_
#include <stdio.h>
int sum(int a, int b);
#endif /* SUM_H_ */

sum.c
#include "sum.h"

int sum(int a, int b){


return (a+b);
}

main.c
#include <stdio.h>
#include "sum.h"

int main(int argc, char **argv){

int x;
x= sum(1, 2);
printf("x = %d \n", x);

return 1;
}

Makefile 
sum: main.o sum.o
gcc -o sum main.o sum.o
main.o: main.c sum.h
gcc -c main.c
sum.o: sum.c sum.h
gcc -c sum.c

 Phân tích cấu trúc Makefile:


Rule: Có thể có nhiều Rule, trong ví dụ trên có 3 rule

Target

Dependency: Cần thiết để tạo ra Target

Action: Câu lệnh compile để tạo ra Target từ Dependency, 


option là "-c" để tạo ra file object từ file source code,
option là "-o" để tạo ra file chương trình nhị phân từ file object
Mỗi Rule có thể không có hoặc có nhiều Action 
Action được thụt lùi vào một Tab (bằng 4 lần blank space) so với Target

 Thứ tự thực hiện:


Khi bạn thực hiện lệnh make, chương trình make sẽ nhảy đến target đầu tiên là sum
với mục đích để tạo ra nó, để làm được điều đó make đi kiểm tra lần lượt (từ trái
qua phải: main.o -> sum.o) xem các dependency của sum đã tồn tại chưa.
Denpendency đầu tiên là main.o chưa có, cần phải tìm rule nào đó mà ở đó main.o
đóng vai trò là target, make tìm ra rule thứ 2 và nó nhảy đến thực hiện rule thứ 2 đề
tạo ra main.o (lưu ý khi nó chạy rule 2 thì cũng giống y như khi chạy rule đầu tiên,
có thể coi như là đệ quy). Sau khi tạo ra main.o, make trở về rule 1 để tiến hành
kiểm tra tiếp xem dependency thứ hai là sum.o đã tồn tại chưa, sum.o chưa có vì
thế make tiến hành các bước tương tự như đối với main.o. Sau khi tất cả các
dependency được tạo ra, make mới có thể tạo ra file chạy cuối cùng là sum.

Vậy make thực hiện theo nguyên tắc / thứ tự như sau:
+ Tạo ra các file object trước (main.o, sum.o)
+ Tạo ra chương trình nhị phân cuối cùng từ các file object đã được tạo ra trước đó
(sum)

 Compile & Execute:


"cd" vào thư mục chứa project và thực hiện lệnh make, chương trình sum sẽ được
tạo ra cùng với 2 file object là sum.o và main.o , chạy thử chương trình:

[khoatn@localhost ~]$
[khoatn@localhost ~]$ cd /home/khoatn/Github/eslinuxprogramming/Makefile
[khoatn@localhost Makefile]$
[khoatn@localhost Makefile]$ make
gcc -c main.c
gcc -c sum.c
gcc -o sum main.o sum.o
[khoatn@localhost Makefile]$
[khoatn@localhost Makefile]$
[khoatn@localhost Makefile]$ ./sum
x = 3
[khoatn@localhost Makefile]$
[khoatn@localhost Makefile]$
3. Nguyên lý biên dịch lại của Makefile
 Việc compile lại project dựa vào hai yếu tố:
+ Thời gian chỉnh sửa (date modified)
+ Cây phụ thuộc trong Makefile

 Có nghĩa là một khi Dependency thay đổi thì Target tương ứng cũng phải
được compile lại.
 Theo Makefile như trong ví dụ trên:
+ Nếu chỉnh sủa sum.h thì main.o và sum.o phải được tạo lại, mặt
khác main.o & sum.o lại là dependency của sum nên sum sẽ được tạo lại

[khoatn@localhost Makefile]$ make


gcc -c main.c
gcc -c sum.c
gcc -o sum main.o sum.o

+ Nếu chỉnh sửa sum.c thì sum.o được tạo lại, đương nhiên sum phụ


thuộc sum.o nên sum sẽ được tạo lại

[khoatn@localhost Makefile]$ make


gcc -c sum.c
gcc -o sum main.o sum.o

 Khi viết Makefile tránh gộp tất cả lại làm một như dưới đây, vì khi đó
nếu một trong các file source code thay đổi thì cũng phải compile lại
tất cả các file khác, điều đó làm mất nhiều thời gian:

Makefile
sum: main.c sum.c sum.h
${CC} -o sum main.c sum.c sum.h
4. Khuôn dạng đầy đủ của Makefile

 sum.h (giữ nguyên)


 sum.c (giữ nguyên)
 main.c (thêm #ifdef ... #endif)

#include <stdio.h>
#include "sum.h"

int main(int argc, char **argv){

int x;
x= sum(1, 2);

#ifdef DEBUG
printf("x = %d \n", x);
#endif

return 1;
}

Makefile

.PHONY: all, install, clean

TARGET=sum

HDRS+= sum.h
CSRCS+= main.c sum.c
CPPSRCS+=

OBJSDIR=./build
OBJS:= $(patsubst %.cpp, $(OBJSDIR)/%.o, $(CPPSRCS))
OBJS+= $(patsubst %.c, $(OBJSDIR)/%.o, $(CSRCS))

CFLAGS += -I./include -DDEBUG -Wall -g


LDFLAGS += -L./lib -lm

CC:= gcc
CXX:= g++

all: ${TARGET}
${TARGET} : $(OBJS)
@echo " [LINK] $@"
@mkdir -p $(shell dirname $@)
@$(CXX) $(OBJS) -o $@ $(LDFLAGS)
$(OBJSDIR)/%.o: %.c $(HDRS)
@echo " [CC] $@"
@mkdir -p $(shell dirname $@)
@$(CC) -c $< -o $@ $(CFLAGS)

$(OBJSDIR)/%.o: %.cpp $(HDRS)


@echo " [CXX] $@"
@mkdir -p $(shell dirname $@)
@$(CXX) -c $< -o $@ $(CFLAGS)

install:
cp -rf ${TARGET} /usr/local/bin

clean:
rm -rf ${OBJSDIR}/*.o
rm -rf ${TARGET}

You might also like