ここでは、2つの問題があります。
問題#1:Cは静的に型付けされた言語です。すべての型情報はコンパイル時に決定されます。タイプ情報はオブジェクトとともにメモリに格納されないため、そのタイプとサイズは実行時に決定できます1。プログラムの実行中に特定のアドレスのメモリを調べる場合、表示されるのはバイトのスラッジだけです。特定のアドレスに実際にオブジェクトが含まれているかどうか、そのオブジェクトのタイプまたはサイズは何か、またはそれらのバイトを(整数、浮動小数点タイプ、または文字列内の文字のシーケンスとして)解釈する方法など、何もわかりません。 )。ソースコードで指定された型情報に基づいて、コードがコンパイルされると、そのすべての情報がマシンコードに組み込まれます。たとえば、関数定義
void foo( int x, double y, char *z )
{
...
}
x
整数、y
浮動小数点値、およびz
へのポインタとして処理する適切なマシンコードを生成するようコンパイラーに指示しchar
ます。関数呼び出しと関数定義の間の引数の数またはタイプの不一致は、コードのコンパイル時にのみ検出されることに注意してください2。タイプ情報がオブジェクトに関連付けられるのは、コンパイルフェーズの間だけです。
問題#2:printf
ある可変引数関数は、型const char * restrict
(フォーマット文字列)の1つの固定パラメーターと、0個以上の追加パラメーターを受け取ります。これらのパラメーターの数と型は、コンパイル時に不明です。
int printf( const char * restrict fmt, ... );
このprintf
関数は、渡された引数自体から、追加の引数の数とタイプを知る方法がありません。スタック(またはレジスター)のバイトのスラッジを解釈する方法を伝えるには、フォーマット文字列に依存する必要があります。さらに良いことに、これは可変関数であるため、特定の型の引数は、デフォルトの型の限られたセットに昇格されます(たとえば、short
に昇格されるint
、float
に昇格されるdouble
など)。
繰り返しになりprintf
ますが、それらを解釈またはフォーマットする方法についての手がかりを与える追加の引数自体に関連する情報はありません。したがって、フォーマット文字列に変換指定子が必要です。
printf
変換指定子は、追加の引数の数とタイプを指定printf
するだけでなく、出力のフォーマット方法(フィールド幅、精度、パディング、位置揃え、ベース(整数タイプの場合は10進数、8進数、16進数)など)も通知することに注意してください。
編集する
コメントでの広範な議論を避けるために(そして、チャットページが私の仕事システムからブロックされているため-はい、私は悪い子です)、ここで最後の2つの質問に対処します。
私がこれを行うと: float b;
float c;
b=3.1;
c=(5.0/9.0)*(b);
最後のステートメントで、コンパイラはbがfloat型であることをどのようにして知るのですか?
変換中、コンパイラは、オブジェクトの名前、タイプ、格納期間、スコープなどに関する情報を格納するテーブル(シンボルテーブルと呼ばれることが多い)を維持します。宣言した b
とc
as float
は、コンパイラがその中b
またはc
中の式を見るたびに、浮動小数点値を処理するためのマシンコードを生成します。
上記のコードを使用して、プログラム全体をラップしました。
/**
* 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;
}
私が使用-g
して-Wa,-aldh
Cのソースコードと交互に生成されたマシンコードのリスト作成にはgccでオプションを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
アセンブリリストの読み方は次のとおりです。
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
ここで注意すべきことが1つあります。生成されたアセンブリコードには、b
またはの記号はありませんc
。それらはソースコードリストにのみ存在します。ときにmain
、実行時に実行するには、のためのスペースb
とc
(いくつかの他のものと一緒に)は、スタックポインタを調整することによって、スタックから割り当てられます。
subq $16, %rsp
コードは、それらのフレームポインタからのオフセットによってそれらのオブジェクトを指す4でb
ある-8フレームポインタに格納されたアドレスからのバイトとc
:である-4以下のように、それからのバイト
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
フロートとして宣言b
しc
ているため、コンパイラーは特に浮動小数点値を処理するマシンコードを生成しました。movsd
、mulsd
、cvtss2sd
命令は、すべての浮動小数点演算を特定し、レジスタである%xmm0
と%xmm1
倍精度浮動小数点値を格納するために使用されます。
を浮動小数点数ではなく整数b
となるようにソースコードを変更するc
と、コンパイラは異なるマシンコードを生成します。
/**
* 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;
}
ギブでコンパイルgcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.c
:
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
ここではなくて、同じ操作だb
とc
整数として宣言しました:
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
これは、タイプ情報がマシンコードに「組み込まれた」と言ったときに私が以前に意味したものです。プログラムが実行されても、そのタイプは調べb
たりc
判別したりしません。それはすでに自分のタイプを知っているべきである生成されたマシンコードに基づきます。
コンパイラが実行時にタイプとサイズを決定する場合、次のプログラムが機能しないのはなぜですか。
float b='H';
printf(" value of b is %c \n",b);
コンパイラにうそをついているので機能しません。あなたはそのことを教えb
ているfloat
、それは浮動小数点値を処理するためのマシンコードを生成しますので、。初期化すると、定数に対応するビットパターン'H'
は、文字値ではなく浮動小数点値として解釈されます。
引数に%c
typeの値を期待する変換指定子を使用すると、コンパイラに再びうそをつきchar
ますb
。このためprintf
、の内容がb
正しく解釈されず、ガベージ出力5が発生します。繰り返しprintf
になりますが、引数自体に基づいて追加の引数の数やタイプを知ることはできません。表示されるのはスタック上のアドレス(またはレジスターの束)だけです。渡された追加の引数とその型が何であるかを伝えるために、フォーマット文字列が必要です。
1. 1つの例外は可変長配列です。それらのサイズは実行時まで確定さsizeof
れないため、コンパイル時にVLA を評価する方法はありません。
2.とにかく、C89以降。それ以前は、コンパイラは関数の戻り値の型の不一致のみを検出できました。関数パラメーターリストの不一致を検出できませんでした。
3.このコードは、gcc 4.1.2を使用して64ビットのSuSE Linux Enterprise 10システムで生成されます。別の実装(コンパイラー/ OS /チップアーキテクチャー)を使用している場合、正確な機械語命令は異なりますが、一般的なポイントは変わりません。コンパイラは、浮動小数点数、整数、文字列などを処理するためのさまざまな命令を生成し
ます。4.実行中のプログラムで関数を呼び出すと、スタックフレーム関数の引数、ローカル変数、および関数呼び出しに続く命令のアドレスを格納するために作成されます。フレームポインターと呼ばれる特別なレジスターは、現在のフレームを追跡するために使用されます。
5.たとえば、上位バイトがアドレス指定されたバイトであるビッグエンディアンシステムを想定します。のビットパターンはとしてH
保存さb
れ0x00000048
ます。ただし、%c
変換指定子は引数がであるべきであることを示すため、char
最初のバイトのみが読み取らprintf
れ、エンコーディングに対応する文字を書き込もうとします0x00
。