23/08/2018, 11:04

Thử thực hiện 4 Stage khi Compile C bằng GCC

GCC biên dịch một file .c thành file chạy trong 4 Stage. Preprocessing (tiền xử lý), Assembly Code Compiling (diên dịch sang mã Assembly), Machine Code Compiling (biên dịch sang mã máy), Linking. Trong compile thông thường dạng $gcc -o HelloWorld ...

GCC biên dịch một file .c thành file chạy trong 4 Stage.

Preprocessing (tiền xử lý), Assembly Code Compiling (diên dịch sang mã Assembly), Machine Code Compiling (biên dịch sang mã máy), Linking.

Trong compile thông thường dạng

Với câu lệnh trên, ta sẽ không thấy kết quả của 3 Stage đầu tiên.

Để hiểu rõ hơn, trong giới hạn hiểu biết, mình sẽ thử thực hiện các Stage bằng tay xem liệu có thể tạo ra file chạy như câu lệnh compile trên hay không.

1. Thực hiện Stage 1 (Preprocessing)

Như trong bài trước, stage này sẽ lấy đầu vào là file .c và cho kết quả đầu ra là file .i (thông thường)

Đầu vào: HelloWorld.c

Đầu ra: HelloWorld.i

Câu lệnh thực hiện:

Hoặc bằng câu lệnh cpp:

File HelloWorld.i khá dài so với HelloWorld.c.

Về nội dung, file HelloWorld.i vẫn là một file source C như bao file Source Code C khác mà thôi, chứ hoàn toàn chưa chuyển sang dạng khác.

2. Thực hiện Stage 2 (Compiling to Assembly Code)

Stage này sẽ chuyển từ source C chứa trong các file HelloWorld.i sang Assembly Code, file HelloWorld.s:

Đầu vào: HelloWorld.i

Đầu ra: HelloWorld.s

Câu lệnh thực hiện:

3. Thực hiện Stage 3 (Compiling to Machine Code)

Bước này sẽ chuyển Assembly Code sang mã máy mà chương trình sẽ chạy:

Đầu vào: HelloWorld.s

Đầu ra:HelloWorld.o

Câu lệnh thực hiện:

as chính là Assembler, một trình biên dịch Assembly.

4. Thực hiện Stage 4 (Linking)

Linker trong Linux là ld hay tên đầy đủ là GNU Linker. Xem thêm ở man ld

  • Lần 1: Ta thấy 3 stage ở trên thực hiện không mấy khó khăn gì.
    Cùng dạng câu lệnh như thế mà thực hiện thì sao.
    Đây là kết quả:

Lần 2: Đọc một chút về Linker trong man page.
Có vẻ puts sinh ra từ printf đang được gọi trong source.
Và hiểu ra một chút rằng, chương trình C ta viết sử dụng hàm printf, hàm này không phải ta tự viết.
Người mới học vẫn gọi là hàm chuẩn, hầu như không quan tâm nó đến từ đâu.
Nhưng ta đang làm manual mà. Bản thân ngôn ngữ C không bao gồm một thư viện, hay hàm nào cả.
Vậy hàm printf lấy từ đâu, ta phải chỉ cho Linker biết. Đó là thư viện libc, chứa những hàm cơ bản mà chúng ta bảo là chuẩn cho Linux.
Tham khảo ví dụ trong
man page,
ta thực hiện lại việc link bằng command:

Lỗi trên do không có file.

Bỏ /lib/crt0.o đi thì sao:

Lỗi trên entry_point (địa chỉ hàm mà CPU sẽ nhảy vào đầu tiên để bắt đầu thực hiện chương trình) chưa được khai báo.

Sửa như sau:

Câu lệnh thành công, file HelloWorld được tạo ra.

Tuy nhiên, khi chạy thì:

Lỗi trên là không có file nào như thế. WTF, lỗi gì lạ vậy, rõ ràng là có mà.

Kiểm tra như sau:

Kiểm tra dynamic loader linker từ file kết quả được build “không manual”.

Tức là bằng câu lệnh:

Ta thử “ép buộc” sử dụng dynamic loader linker hiện có thì sao.

Chạy được nhưng bị Segmentation fault.

Để hiểu kĩ tại sao xảy ra lỗi này, chắc có vẻ mất thời gian.

Lần 4: Bắt chước câu lệnh biên dịch auto thì sao nhỉ? GCC đã thực hiện quá trình linking khi tạo ra file HelloWorld_auto ở trên như thế nào? Thật may, sau một hồi hỏi thầy GG. Ta có thể thấy được toàn bộ tham số của câu lệnh biên dịch ở trên bằng tham số -v vào câu lệnh biên dịch.

Kết quả từ câu lệnh trên khá rắc rối, (có lẽ cần 1 bài khác để nói kĩ hơn về nó).

Tuy nhiên ta chỉ quan tâm đến đoạn tham số của collect2 (chính là Linker mà GCC sử dụng cho ngôn ngữ C) mà thôi.

Nó như thế này:

Nhìn nản luôn, edit lại chút cho “rắc rối hơn”