Có hai vấn đề đang diễn ra ở đây:
Vấn đề # 1: C là một ngôn ngữ gõ tĩnh ; tất cả các thông tin loại được xác định tại thời gian biên dịch. Không có thông tin loại được lưu trữ với bất kỳ đối tượng nào trong bộ nhớ sao cho loại và kích thước của nó có thể được xác định tại thời điểm chạy 1 . Nếu bạn kiểm tra bộ nhớ tại bất kỳ địa chỉ cụ thể nào trong khi chương trình đang chạy, tất cả những gì bạn sẽ thấy là bùn của byte; không có gì để cho bạn biết liệu địa chỉ cụ thể đó có thực sự chứa một đối tượng hay không, loại hoặc kích thước của đối tượng đó là gì hoặc làm thế nào để giải thích các byte đó (dưới dạng số nguyên, hoặc loại dấu phẩy động hoặc chuỗi ký tự trong chuỗi, v.v. ). Tất cả thông tin đó được đưa vào mã máy khi mã được biên dịch, dựa trên thông tin loại được chỉ định trong mã nguồn; ví dụ: định nghĩa hàm
void foo( int x, double y, char *z )
{
...
}
báo cho trình biên dịch tạo mã máy thích hợp để xử lý x
như một số nguyên, y
dưới dạng giá trị dấu phẩy động và z
như một con trỏ tới char
. Lưu ý rằng bất kỳ sự không phù hợp nào về số lượng hoặc loại đối số giữa lệnh gọi hàm và định nghĩa hàm chỉ được phát hiện khi mã đang được biên dịch 2 ; Chỉ trong giai đoạn biên dịch, bất kỳ thông tin loại nào cũng được liên kết với một đối tượng.
Vấn đề # 2: printf
là một hàm matrixdic ; nó nhận một tham số cố định của loại const char * restrict
(chuỗi định dạng), cùng với 0 hoặc nhiều tham số bổ sung, số lượng và loại không được biết tại thời điểm biên dịch:
int printf( const char * restrict fmt, ... );
Các printf
chức năng không có cách nào để biết những gì số lượng và loại của các đối số bổ sung là từ những lập luận thông qua bản thân; nó phải dựa vào chuỗi định dạng để cho nó biết cách diễn giải bùn của byte trên ngăn xếp (hoặc trong các thanh ghi). Thậm chí tốt hơn, bởi vì đó là một hàm biến đổi, các đối số với một số loại nhất định được quảng cáo thành một tập hợp các loại mặc định hạn chế (ví dụ: short
được thăng cấp int
, float
được thăng cấp double
, v.v.).
Một lần nữa, không có thông tin liên quan đến chính các đối số bổ sung để đưa ra printf
bất kỳ manh mối nào về cách diễn giải hoặc định dạng chúng. Do đó, cần phải có các chỉ định chuyển đổi trong chuỗi định dạng.
Lưu ý rằng ngoài việc cho printf
biết số lượng và loại đối số bổ sung, các công cụ xác định chuyển đổi cũng cho biết printf
cách định dạng đầu ra (độ rộng trường, độ chính xác, phần đệm, chứng minh, cơ sở (thập phân, bát phân hoặc hex cho các kiểu số nguyên), v.v.).
Biên tập
Để tránh thảo luận rộng rãi trong các bình luận (và vì trang trò chuyện bị chặn khỏi hệ thống làm việc của tôi - vâng tôi là một đứa trẻ hư), tôi sẽ giải quyết hai câu hỏi cuối ở đây.
NẾU tôi làm điều này: float b;
float c;
b=3.1;
c=(5.0/9.0)*(b);
Trong câu lệnh cuối làm thế nào trình biên dịch biết rằng b là kiểu float?
Trong quá trình dịch, trình biên dịch duy trì một bảng (thường được gọi là bảng ký hiệu ) lưu trữ thông tin về tên, loại, thời lượng lưu trữ, phạm vi, v.v. Bạn đã khai báo b
và c
như float
vậy, bất cứ khi nào trình biên dịch nhìn thấy một biểu thức có b
hoặc c
trong đó, nó sẽ tạo mã máy để xử lý giá trị dấu phẩy động.
Tôi lấy mã của bạn ở trên và gói một chương trình đầy đủ xung quanh nó:
/**
* c1.c
*/
#include <stdio.h>
int main( void )
{
float b;
float c;
b = 3.1;
c = (5.0 / 9.0) * b;
printf( "c = %f\n", c );
return 0;
}
Tôi đã sử dụng -g
và -Wa,-aldh
các tùy chọn với gcc để tạo danh sách mã máy được tạo xen kẽ với mã nguồn C 3 :
GAS LISTING /tmp/ccmGgGG2.s page 1
1 .file "c1.c"
9 .Ltext0:
10 .section .rodata
11 .LC2:
12 0000 63203D20 .string "c = %f\n"
12 25660A00
13 .align 8
14 .LC1:
15 0008 721CC771 .long 1908874354
16 000c 1CC7E13F .long 1071761180
17 .text
18 .globl main
20 main:
21 .LFB2:
22 .file 1 "c1.c"
1:c1.c **** #include <stdio.h>
2:c1.c **** int main( void )
3:c1.c **** {
23 .loc 1 3 0
24 0000 55 pushq %rbp
25 .LCFI0:
26 0001 4889E5 movq %rsp, %rbp
27 .LCFI1:
28 0004 4883EC10 subq $16, %rsp
29 .LCFI2:
4:c1.c **** float b;
5:c1.c **** float c;
6:c1.c **** b = 3.1;
30 .loc 1 6 0
31 0008 B8666646 movl $0x40466666, %eax
31 40
32 000d 8945F8 movl %eax, -8(%rbp)
7:c1.c **** c = (5.0 / 9.0) * b;
33 .loc 1 7 0
34 0010 F30F5A4D cvtss2sd -8(%rbp), %xmm1
34 F8
35 0015 F20F1005 movsd .LC1(%rip), %xmm0
35 00000000
36 001d F20F59C1 mulsd %xmm1, %xmm0
37 0021 F20F5AC0 cvtsd2ss %xmm0, %xmm0
38 0025 F30F1145 movss %xmm0, -4(%rbp)
38 FC
8:c1.c ****
9:c1.c **** printf( "c = %f\n", c );
39 .loc 1 9 0
40 002a F30F5A45 cvtss2sd -4(%rbp), %xmm0
40 FC
41 002f BF000000 movl $.LC2, %edi
41 00
42 0034 B8010000 movl $1, %eax
42 00
43 0039 E8000000 call printf
43 00
10:c1.c **** return 0;
44 .loc 1 10 0
45 003e B8000000 movl $0, %eax
GAS LISTING /tmp/ccmGgGG2.s page 2
11:c1.c **** }
46 .loc 1 11 0
47 0043 C9 leave
48 0044 C3 ret
Dưới đây là cách đọc danh sách lắp ráp:
40 002a F30F5A45 cvtss2sd -4(%rbp), %xmm0
40 FC
^ ^ ^ ^ ^
| | | | |
| | | | +-- Instruction operands
| | | +------------------ Instruction mnemonic
| | +---------------------------------------- Actual machine code (instruction and operands)
| +--------------------------------------------- Byte offset of instruction from subroutine entry point
+------------------------------------------------ Line number of assembly listing
Một điều cần lưu ý ở đây. Trong mã lắp ráp được tạo, không có ký hiệu cho b
hoặc c
; chúng chỉ tồn tại trong danh sách mã nguồn. Khi main
thực thi trong thời gian chạy, không gian cho b
và c
(cùng với một số nội dung khác) được phân bổ từ ngăn xếp bằng cách điều chỉnh con trỏ ngăn xếp:
subq $16, %rsp
Mã tham chiếu đến các đối tượng đó bằng phần bù của chúng từ con trỏ khung 4 , với b
-8 byte từ địa chỉ được lưu trong con trỏ khung và c
là -4 byte từ nó, như sau:
7:c1.c **** c = (5.0 / 9.0) * b;
.loc 1 7 0
cvtss2sd -8(%rbp), %xmm1 ;; converts contents of b from single- to double-
;; precision float, stores result to floating-
;; point register xmm1
movsd .LC1(%rip), %xmm0 ;; writes the pre-computed value of 5.0/9.0
;; to floating point register xmm0
mulsd %xmm1, %xmm0 ;; multiply contents of xmm1 by xmm0, store result
;; in xmm0
cvtsd2ss %xmm0, %xmm0 ;; convert result in xmm0 from double- to single-
;; precision float
movss %xmm0, -4(%rbp) ;; save result to c
Vì bạn đã khai báo b
và c
dưới dạng float, trình biên dịch đã tạo mã máy để xử lý cụ thể các giá trị dấu phẩy động; các movsd
, mulsd
, cvtss2sd
hướng dẫn đều là đặc trưng cho hoạt động nổi-điểm, và các thanh ghi %xmm0
và %xmm1
được sử dụng để lưu trữ gấp đôi độ chính xác giá trị dấu chấm động.
Nếu tôi thay đổi mã nguồn sao cho b
và c
là số nguyên thay vì số float, trình biên dịch sẽ tạo mã máy khác nhau:
/**
* c2.c
*/
#include <stdio.h>
int main( void )
{
int b;
int c;
b = 3;
c = (9 / 4) * b; // changed these values since integer 5/9 == 0, making for
// some really boring machine code.
printf( "c = %d\n", c );
return 0;
}
Biên dịch với gcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.c
cho:
GAS LISTING /tmp/ccyxHwid.s page 1
1 .file "c2.c"
9 .Ltext0:
10 .section .rodata
11 .LC0:
12 0000 63203D20 .string "c = %d\n"
12 25640A00
13 .text
14 .globl main
16 main:
17 .LFB2:
18 .file 1 "c2.c"
1:c2.c **** #include <stdio.h>
2:c2.c **** int main( void )
3:c2.c **** {
19 .loc 1 3 0
20 0000 55 pushq %rbp
21 .LCFI0:
22 0001 4889E5 movq %rsp, %rbp
23 .LCFI1:
24 0004 4883EC10 subq $16, %rsp
25 .LCFI2:
4:c2.c **** int b;
5:c2.c **** int c;
6:c2.c **** b = 3;
26 .loc 1 6 0
27 0008 C745F803 movl $3, -8(%rbp)
27 000000
7:c2.c **** c = (9 / 4) * b;
28 .loc 1 7 0
29 000f 8B45F8 movl -8(%rbp), %eax
30 0012 01C0 addl %eax, %eax
31 0014 8945FC movl %eax, -4(%rbp)
8:c2.c ****
9:c2.c **** printf( "c = %d\n", c );
32 .loc 1 9 0
33 0017 8B75FC movl -4(%rbp), %esi
34 001a BF000000 movl $.LC0, %edi
34 00
35 001f B8000000 movl $0, %eax
35 00
36 0024 E8000000 call printf
36 00
10:c2.c **** return 0;
37 .loc 1 10 0
38 0029 B8000000 movl $0, %eax
38 00
11:c2.c **** }
39 .loc 1 11 0
40 002e C9 leave
41 002f C3 ret
Đây là hoạt động tương tự, nhưng với b
và được c
khai báo là số nguyên:
7:c2.c **** c = (9 / 4) * b;
.loc 1 7 0
movl -8(%rbp), %eax ;; copy value of b to register eax
addl %eax, %eax ;; since 9/4 == 2 (integer arithmetic), double the
;; value in eax
movl %eax, -4(%rbp) ;; write result to c
Đây là những gì tôi muốn nói trước đó khi tôi nói rằng thông tin loại đã được "nướng" vào mã máy. Khi chương trình chạy, nó không kiểm tra b
hoặc c
xác định loại của chúng; họ đã biết loại của họ nên dựa trên mã máy được tạo.
Nếu trình biên dịch xác định loại và kích thước trong thời gian chạy thì tại sao chương trình sau không hoạt động:
float b='H';
printf(" value of b is %c \n",b);
Nó không hoạt động vì bạn đang nói dối với trình biên dịch. Bạn nói với nó b
là a float
, vì vậy nó sẽ tạo mã máy để xử lý các giá trị dấu phẩy động. Khi bạn khởi tạo nó, mẫu bit tương ứng với hằng số 'H'
sẽ được hiểu là giá trị dấu phẩy động, không phải giá trị ký tự.
Bạn nói dối trình biên dịch một lần nữa khi bạn sử dụng trình %c
xác định chuyển đổi, dự kiến giá trị của kiểu char
, cho đối số b
. Vì điều này, printf
sẽ không diễn giải b
chính xác nội dung và bạn sẽ kết thúc với đầu ra rác 5 . Một lần nữa, printf
không thể biết số lượng hoặc loại của bất kỳ đối số bổ sung nào dựa trên chính các đối số đó; tất cả những gì nó nhìn thấy là một địa chỉ trên ngăn xếp (hoặc một loạt các thanh ghi). Nó cần chuỗi định dạng để cho nó biết những đối số bổ sung nào đã được thông qua và loại của chúng là gì.
1. Một ngoại lệ là mảng có độ dài thay đổi; vì kích thước của chúng không được thiết lập cho đến thời gian chạy, không có cách nào để đánh giá sizeof
về VLA tại thời điểm biên dịch.
2. Kể từ C89, dù sao đi nữa. Trước đó, trình biên dịch chỉ có thể bắt các lỗi không khớp trong kiểu trả về hàm; nó không thể phát hiện sự không phù hợp trong danh sách tham số chức năng.
3. Mã này được tạo trên hệ thống SuSE Linux Enterprise 10 64 bit bằng gcc 4.1.2. Nếu bạn đang thực hiện một cách triển khai khác (kiến trúc trình biên dịch / hệ điều hành / chip), thì các hướng dẫn chính xác của máy sẽ khác, nhưng điểm chung vẫn sẽ giữ; trình biên dịch sẽ tạo ra các hướng dẫn khác nhau để xử lý float và ints so với chuỗi, v.v.
4. Khi bạn gọi một hàm trong chương trình đang chạy, khung stackđược tạo để lưu trữ các đối số hàm, biến cục bộ và địa chỉ của lệnh theo lệnh gọi hàm. Một thanh ghi đặc biệt gọi là con trỏ khung được sử dụng để theo dõi khung hiện tại.
5. Ví dụ, giả sử một hệ thống cuối lớn trong đó byte thứ tự cao là byte được đánh địa chỉ. Mẫu bit cho H
sẽ được lưu trữ b
dưới dạng 0x00000048
. Tuy nhiên, vì trình %c
xác định chuyển đổi chỉ ra rằng đối số phải là a char
, nên chỉ đọc byte đầu tiên, do đó printf
sẽ cố gắng viết ký tự tương ứng với mã hóa 0x00
.