顯示具有 c_spec 標籤的文章。 顯示所有文章
顯示具有 c_spec 標籤的文章。 顯示所有文章

2022年3月13日 星期日

printf 的 integer promotion

巧者勞力, 智者勞心
p.c
 1 #include <stdio.h>
 2
 3 int main(int argc, char *argv[])
 4 {
 5   unsigned char a=0x80;
 6   signed char b=0x80;
 7
 8   printf("unsigned char a: %#x, signed char b: %#x\n", a, b);
 9   printf("unsigned char a: %#x (%d), signed char b: %hhx (%hhd)\n", a, a, b, b);
10   return 0;
11 }

list 1 執行結果
 1 unsigned char a: 0x80, signed char b: 0xffffff80
 2 unsigned char a: 0x80 (128), signed char b: 80 (-128)


list 1 是執行結果, L1 顯示了 signed char b: 0xffffff80, 不是預期的 0x80, 有點奇怪是吧? 引用之前的文章 - compiler [7] - code generator - funcall call, pass argument
function 參數的傳遞比想像中複雜, 當 function 沒有 prototype 時或是使用 K&R style 的宣告或是 ... 這種參數 - ex: printf(const char *format, ...), 會發動 integer promtion, 這很好理解, 可以參考《“对于那些没有原型的函数,传递给函数的实参将进行缺省参数提升”是什么意思?》
printf 的參數是 ... 所以會發動 integer promtion, integer promtion 之後, signed char b 0x80 的 -128 變成了 4bytes 的 -128, 也就是 0xffffff80, 這便是 %#x 印出 0xffffff80 的原因。

cpu 用什麼指令做這件事情, 反組譯一下觀察, list 2 L716, 717 的指令 movsbl, movzbl 就是在做這件事情。由於比較熟悉 x86-32, 所以編譯成 x86-32 來觀察, 其他平台應該也有類似的指令, 就不一一觀察了。

list 2. p.c.dis 編譯指令 gcc -static -no-pie -fno-pic -m32 -g p.c -o p
     1 
     2 p:     file format elf32-i386
   705 08049815 <main>:
   706  8049815:	8d 4c 24 04          	lea    0x4(%esp),%ecx
   707  8049819:	83 e4 f0             	and    $0xfffffff0,%esp
   708  804981c:	ff 71 fc             	push   -0x4(%ecx)
   709  804981f:	55                   	push   %ebp
   710  8049820:	89 e5                	mov    %esp,%ebp
   711  8049822:	53                   	push   %ebx
   712  8049823:	51                   	push   %ecx
   713  8049824:	83 ec 10             	sub    $0x10,%esp
   714  8049827:	c6 45 f7 80          	movb   $0x80,-0x9(%ebp) # unsigned char a=0x80
715 804982b: c6 45 f6 80 movb $0x80,-0xa(%ebp) # signed char b=0x80
716 804982f: 0f be 55 f6 movsbl -0xa(%ebp),%edx # signed char b=0x80 717 8049833: 0f b6 45 f7 movzbl -0x9(%ebp),%eax # unsigned char a=0x80 718 8049837: 83 ec 04 sub $0x4,%esp 719 804983a: 52 push %edx 720 804983b: 50 push %eax 721 804983c: 68 08 30 0b 08 push $0x80b3008 722 8049841: e8 da 85 00 00 call 8051e20 <_IO_printf> 723 8049846: 83 c4 10 add $0x10,%esp 724 8049849: 0f be 5d f6 movsbl -0xa(%ebp),%ebx 725 804984d: 0f be 4d f6 movsbl -0xa(%ebp),%ecx 726 8049851: 0f b6 55 f7 movzbl -0x9(%ebp),%edx 727 8049855: 0f b6 45 f7 movzbl -0x9(%ebp),%eax 728 8049859: 83 ec 0c sub $0xc,%esp 729 804985c: 53 push %ebx 730 804985d: 51 push %ecx 731 804985e: 52 push %edx 732 804985f: 50 push %eax 733 8049860: 68 34 30 0b 08 push $0x80b3034 734 8049865: e8 b6 85 00 00 call 8051e20 <_IO_printf> 735 804986a: 83 c4 20 add $0x20,%esp 736 804986d: b8 00 00 00 00 mov $0x0,%eax 737 8049872: 8d 65 f8 lea -0x8(%ebp),%esp 738 8049875: 59 pop %ecx 739 8049876: 5b pop %ebx 740 8049877: 5d pop %ebp 741 8049878: 8d 61 fc lea -0x4(%ecx),%esp 742 804987b: c3 ret

另外 printf 提供了 %hh 來印出 signed char 或是 unsigned char 變數。

ref:

2017年4月7日 星期五

c 語言中的 0x7fffffff 和 0xffff0000 是什麼 type?

在 c 程式碼上的一個數字, 他們是什麼型別呢? int, unsigned int, long int ... 你會不會有類似的疑惑呢? 而不知道他們是什麼型別有什麼要緊的呢? 有經驗的程式員馬上就聯想到會不會有 overflow 的問題。

5+253 如果 5 是 unsigned char, 253 是 unsigned char, 那加起來不就超過 char 的大小了嗎? 但是
cout << 5+300 << endl;
可以正常印出 258, 而不是被截掉的數值。

那這兩個數字 0x7fffffff 和 0xffff0000 是什麼 type 呢?

為什麼 0x7fffffff 不是 unsigned int?《linux c 編程 一站式學習》有答案, 我想驗證看看是不是真的, 其實應該讀 c spec, 但你知道的 ...

該怎麼驗證呢? 使用 c++ rtti, 以下程式碼使用 c++ rtti 來得到 type。

c++filt -t 可以把神秘的字母轉成看得懂的 type。

a.cpp
 1 #include <typeinfo>
 1 #include <typeinfo>
 2 #include <iostream>
 3 using namespace std;
 4 
 5 #include <sys/types.h>
 6 #include <sys/stat.h>
 7 #include <fcntl.h>
 8 #include <unistd.h>
 9 #include <sys/mman.h>
10 
11 
12 int main(int argc, char *argv[])
13 {
14   cout << "0x7fffffff type:" << typeid(0x7fffffff).name() << endl;
15   cout << "0xffff0000 type: " << typeid(0xffff0000).name() << endl;
16   cout << "off_t type: " << typeid(off_t).name() << endl;
17   return 0;
18 }

51 descent@debian64:tmp$ ./a.out |c++filt 
52 0x7fffffff type:i
53 0xffff0000 type: j
54 off_t type: l


71 descent@debian64:tmp$ ./a.out |c++filt -t
72 0x7fffffff type:int
73 0xffff0000 type: unsigned int
74 off_t type: long


再來是一個很特別的數字, -2147483648。
測試環境是:
x86 32bit/linux
gcc 5.4.0
int: 32bit
longlong: 64bit

-2147483648 可以用 int 大小 (32bit) 裝下, 0x80000000 是其 16 進位表達式。
2147483648 卻要用 long long 大小 (64bit) 才能裝下, 0x0000000080000000 是其 16 進位表達式。

int.cpp
 1 #include <cstdio>
 2 #include <iostream>
 3 #include <typeinfo>
 4
 5 using namespace std;
 6
 7
 8 int main(int argc, char *argv[])
 9 {
10   int i = -2147483648;
11   cout <<  typeid(-2147483648).name() << endl;
12   printf("%d\n", -2147483648);
13 }

descent@debian64:ctrl_proc$ g++ -m32 -Wall -std=c++11 int.cpp -o int
int.cpp: In function ‘int main(int, char**)’:
int.cpp:12:29: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long long int’ [-Wformat=]
   printf("%d\n", -2147483648);
                             ^
int.cpp:10:7: warning: unused variable ‘i’ [-Wunused-variable]
   int i = -2147483648;
       ^
descent@debian64:ctrl_proc$ ./int |c++filt -t
long long
-2147483648

-2147483648 type 是 long long, 而不是 int, 雖然 int 裝的下 -2147483648, 怎麼回事, 從 AST 來看這個 printf("%d\n", -2147483648); 運算式。

main |int |func |global
           |
       func_body
           |
         printf
         __|___
         |    |
        %d\n  -
              |
          2147483648

-2147483648 被分成 -, 2147483648, 而 2147483648 是 long long type, 做了 - 運算後, 依然是 long long, 所以 -2147483648 是 long long, 也就是 0xffffffff80000000, 而 %d 預期是 int type, 所以編譯器便發出 warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long long int’ [-Wformat=] 警告。

有趣的是 int.cpp L10, int i = -2147483648; 卻不會發警告, int i 是可以存進 -2147483648, 真奇怪, -2147483648 的 type 是 long long, 所以會有被截掉的可能, 但是由於 -2147483648 可以裝進 int, 所以不用擔心會被截掉。

《linux c 編程 一站式學習》作者很謹慎, 沒亂唬爛我。

c++ 是不是好用呢? 有一些高階語言的特性與低階語言的執行速度, 它是很了不起的語言。

ref:
Strange output of std::typeid::name()

2016年4月22日 星期五

C 語言的 usual arithmetic conversion

程式在下面的文章哦!

《ptt [問題] 十三誡之七的疑問》有一個這樣的問題。

a5.c
 1 #include <stdio.h>
 2 
 3 int main(int argc, char *argv[])
 4 {
 5   unsigned int a = 0;
 6   for(int i = 9 ; i >= a ; i--) 
 7   {  
 8     printf("i: %x\n", i);
 9   }
10     
11   return 0;
12 }

a5.c 執行結果是
i: 9
i: 8
i: 7
i: 6
i: 5
i: 4
i: 3
i: 2
i: 1
i: 0
i: ffffffff
i: fffffffe
i: fffffffd
i: fffffffc
i: fffffffb
i: fffffffa
i: fffffff9
i: fffffff8
i: fffffff7
i: fffffff6
i: fffffff5
i: fffffff4
...
...
... 

i 被當成 unsigned int 看待了, 這是怎麼回事?

《tinlans Re: [問題] 十三誡之七的疑問》回覆的很詳細, 不過你可能被 c spec 的英文和複雜的規定搞得更亂了 (是少我是這樣), 《inux c 編程 一站式學習》 3.2. Usual Arithmetic Conversion 有簡單一點的說明。

a5.c 符合這點:
否则,如果一边是无符号数另一边是有符号数,无符号数的Rank不低于有符号数的Rank,则把有符号数转成另一边的无符号类型。例如unsigned long和int做算术运算时都转成unsigned long,unsigned long和long做算术运算时也都转成unsigned long。

其實就是 fig 1 的中文翻譯, 英文果然不是很好懂。

fig 1 n1570 c11 spec draft sual arithmetic conversion
寄件者 ??

所以 L6 int i 被轉成 unsigned int i 之後才和 a 做比較; i >= a 是以 unsigned int 在做比較, unsigned int 自然沒有小於零的數, 所以條件便一直成立。

這並不是 Integer Promotion, 而是 usual arithmetic conversion, 別搞錯了。

可见有符号和无符号整数的转换规则是十分复杂的,虽然这是有明确规定的,不属于阴暗角落,但为了程序的可读性不应该依赖这些规则来写代码。我讲这些规则,不是为了让你用,而是为了让你了解有符号数和无符号数混用会非常麻烦,从而避免触及这些规则,并且在程序出错时记得往这上面找原因。所以这些规则不需要牢记,但要知道有这么回事,以便在用到的时候能找到我书上的这一段。

感謝作者宋劲杉的苦口婆心。

如果你對《inux c 編程 一站式學習》這本書寫的有懷疑, 又看不懂 c spec, the c programming language 繁體中文版 A6.5 也介紹了 usual arithmetic conversions 的規則, 寫的不是很詳細, 不過對照程式應該可以看懂為什麼是符合該轉換規則。

fig 2 一樣在說明這件事。
fig 2 C语言程序设计现代方法第2版

C11: ISO/IEC 9899:2011 specification 似乎沒有想像中的恐怖, 大概 180 頁左右, 後面是標準程式庫。

ref:
C 語言的潛規則型態轉換