【发布时间】:2019-08-22 08:50:24
【问题描述】:
我正在开发的软件有两个版本,一个用于 int 大小为 16 位的嵌入式系统,另一个用于在 int 大小为 32 位的桌面上进行测试。我正在使用来自<stdint.h> 的固定宽度整数类型,但整数提升规则仍然取决于 int 的大小。
理想情况下,由于整数提升,我希望像下面的代码打印65281(整数提升到 16 位)而不是 4294967041(整数提升到 32 位),以便它完全匹配上的行为嵌入式系统。我想确保在我的桌面测试期间给出一个答案的代码在嵌入式系统上给出完全相同的答案。 GCC 或 Clang 的解决方案都可以。
#include <stdio.h>
#include <stdint.h>
int main(void){
uint8_t a = 0;
uint8_t b = -1;
printf("%u\n", a - b);
return 0;
}
编辑:
我给出的示例可能不是最好的示例,但我确实希望整数提升为 16 位而不是 32 位。举个例子:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void){
uint16_t a = 0;
uint16_t b = 1;
uint16_t c = a - 2; // "-2": 65534
uint16_t d = (a - b) / (a - c);
printf("%" PRIu16 "\n", d);
return 0;
}
在 32 位系统上的输出为 0,因为在提升为(有符号)int 后从整数除法截断,而不是 32767。
到目前为止,最好的答案似乎是使用模拟器,这不是我所希望的,但我想确实有道理。从理论上讲,编译器似乎应该有可能生成表现得好像 int 的大小是 16 位的代码,但我想在实践中没有简单的方法来做到这一点,这可能并不令人惊讶,并且对这种模式和任何必要的运行时支持的需求可能并不多。
编辑 2:
这是我目前所探索的:事实上,有一个 GCC 版本以 16 位模式在 i386 为目标https://github.com/tkchia/gcc-ia16。输出是一个 DOS COM 文件,可以在 DOSBox 中运行。比如这两个文件:
test.c
#include <stdint.h>
uint16_t result;
void test16(void){
uint16_t a = 0;
uint16_t b = 1;
uint16_t c = a - 2; // "-2": 65534
result = (a - b) / (a - c);
}
main.c
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
extern uint16_t result;
void test16(void);
int main(void){
test16();
printf("result: %" PRIu16"\n", result);
return 0;
}
可以编译
$ ia16-elf-gcc -Wall test16.c main.c -o a.com
生成a.com,可以在DOSBox中运行。
D:\>a
result: 32767
进一步研究,ia16-elf-gcc 实际上会生成一个 32 位 elf 作为中间体,尽管默认情况下最终的链接输出是一个 COM 文件:
$ ia16-elf-gcc -Wall -c test16.c -o test16.o
$ file test16.o
test16.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
我可以强制它与使用常规 GCC 编译的 main.c 链接,但不足为奇的是,生成的可执行段错误。
$ gcc -m32 -c main.c -o main.o
$ gcc -m32 -Wl,-m,elf_i386,-s,-o,outfile test16.o main.o
$ ./outfile
Segmentation fault (core dumped)
从here 的帖子看来,理论上应该可以将 ia16-elf-gcc 的 16 位代码输出链接到 32 位代码,尽管我实际上不确定如何。然后还有在 64 位操作系统上实际运行 16 位代码的问题。更理想的是一个编译器,它仍然使用常规的 32 位/64 位寄存器和指令来执行算术,但通过库调用来模拟算术,类似于在(非 64-位)微控制器。
我能找到的在 x86-64 上实际运行 16 位代码的最接近的是 here,这似乎是实验性的/完全未维护。在这一点上,仅使用模拟器开始似乎是最好的解决方案,但我会再等一会儿,看看其他人是否有任何想法。
编辑 3
我将继续接受 antti 的回答,尽管这不是我希望听到的答案。如果有人对 ia16-elf-gcc 的输出感兴趣(我以前从未听说过 ia16-elf-gcc),这里是反汇编:
$ objdump -M intel -mi386 -Maddr16,data16 -S test16.o > test16.s
请注意,您必须指定它是 16 位代码,否则 objdump 会将其解释为 32 位代码,从而映射到不同的指令(请参阅下文)。
test16.o: file format elf32-i386
Disassembly of section .text:
00000000 <test16>:
0: 55 push bp ; save frame pointer
1: 89 e5 mov bp,sp ; copy SP to frame pointer
3: 83 ec 08 sub sp,0x8 ; allocate 4 * 2bytes on stack
6: c7 46 fe 00 00 mov WORD PTR [bp-0x2],0x0 ; uint16_t a = 0
b: c7 46 fc 01 00 mov WORD PTR [bp-0x4],0x1 ; uint16_t b = 1
10: 8b 46 fe mov ax,WORD PTR [bp-0x2] ; ax = a
13: 83 c0 fe add ax,0xfffe ; ax -= 2
16: 89 46 fa mov WORD PTR [bp-0x6],ax ; uint16_t c = ax = a - 2
19: 8b 56 fe mov dx,WORD PTR [bp-0x2] ; dx = a
1c: 8b 46 fc mov ax,WORD PTR [bp-0x4] ; ax = b
1f: 29 c2 sub dx,ax ; dx -= b
21: 89 56 f8 mov WORD PTR [bp-0x8],dx ; temp = dx = a - b
24: 8b 56 fe mov dx,WORD PTR [bp-0x2] ; dx = a
27: 8b 46 fa mov ax,WORD PTR [bp-0x6] ; ax = c
2a: 29 c2 sub dx,ax ; dx -= c (= a - c)
2c: 89 d1 mov cx,dx ; cx = dx = a - c
2e: 8b 46 f8 mov ax,WORD PTR [bp-0x8] ; ax = temp = a - b
31: 31 d2 xor dx,dx ; clear dx
33: f7 f1 div cx ; dx:ax /= cx (unsigned divide)
35: 89 c0 mov ax,ax ; (?) ax = ax
37: 89 c0 mov ax,ax ; (?) ax = ax
39: a3 00 00 mov ds:0x0,ax ; ds[0] = ax
3c: 90 nop
3d: 89 c0 mov ax,ax ; (?) ax = ax
3f: 89 ec mov sp,bp ; restore saved SP
41: 5d pop bp ; pop saved frame pointer
42: 16 push ss ; ss
43: 1f pop ds ; ds =
44: c3 ret
在GDB中调试程序,这条指令导致segfault
movl $0x46c70000,-0x2(%esi)
这是用于设置 a 和 b 值的前两条移动指令,用 32 位模式解码的指令解释。相关反汇编(不指定16位模式)如下:
$ objdump -M intel -S test16.o > test16.s && cat test16.s
test16.o: file format elf32-i386
Disassembly of section .text:
00000000 <test16>:
0: 55 push ebp
1: 89 e5 mov ebp,esp
3: 83 ec 08 sub esp,0x8
6: c7 46 fe 00 00 c7 46 mov DWORD PTR [esi-0x2],0x46c70000
d: fc cld
下一步将尝试找出将处理器置于 16 位模式的方法。它甚至不必是实模式(谷歌搜索大多会找到 x86 16 位实模式的结果),它甚至可以是 16 位保护模式。但在这一点上,使用模拟器绝对是最好的选择,这更多是出于我的好奇心。这也是 x86 特有的。作为参考,这里是在 32 位模式下编译的同一个文件,它隐式提升为 32 位带符号的 int(从运行 gcc -m32 -c test16.c -o test16_32.o && objdump -M intel -S test16_32.o > test16_32.s):
test16_32.o: file format elf32-i386
Disassembly of section .text:
00000000 <test16>:
0: 55 push ebp ; save frame pointer
1: 89 e5 mov ebp,esp ; copy SP to frame pointer
3: 83 ec 10 sub esp,0x10 ; allocate 4 * 4bytes on stack
6: 66 c7 45 fa 00 00 mov WORD PTR [ebp-0x6],0x0 ; uint16_t a = 0
c: 66 c7 45 fc 01 00 mov WORD PTR [ebp-0x4],0x1 ; uint16_t b = 0
12: 0f b7 45 fa movzx eax,WORD PTR [ebp-0x6] ; eax = a
16: 83 e8 02 sub eax,0x2 ; eax -= 2
19: 66 89 45 fe mov WORD PTR [ebp-0x2],ax ; uint16_t c = (uint16_t) (a-2)
1d: 0f b7 55 fa movzx edx,WORD PTR [ebp-0x6] ; edx = a
21: 0f b7 45 fc movzx eax,WORD PTR [ebp-0x4] ; eax = b
25: 29 c2 sub edx,eax ; edx -= b
27: 89 d0 mov eax,edx ; eax = edx (= a - b)
29: 0f b7 4d fa movzx ecx,WORD PTR [ebp-0x6] ; ecx = a
2d: 0f b7 55 fe movzx edx,WORD PTR [ebp-0x2] ; edx = c
31: 29 d1 sub ecx,edx ; ecx -= edx (= a - c)
33: 99 cdq ; EDX:EAX = EAX sign extended (= a - b)
34: f7 f9 idiv ecx ; EDX:EAX /= ecx
36: 66 a3 00 00 00 00 mov ds:0x0,ax ; ds = (uint16_t) ax
3c: 90 nop
3d: c9 leave ; esp = ebp (restore stack pointer), pop ebp
3e: c3 ret
【问题讨论】:
-
你不能,因为底层 ABI 期望你的程序有一个指定的对齐方式。为此,您必须重新编译整个系统。
that it exactly matches the behavior on the embedded system所以如果你想便携,不要使用int。a-b表达式的类型是int- 将其转换为uint8_t并使用PRNu8打印它。如果您使用long和int类型,您的程序将永远无法移植。该行应该是printf("% "PRNu8 "\n", (uint8_t)(a - b));- 这样它将在任何架构任何系统上打印1。 -
为什么不使用强制转换为 uint16? printf("%u\n", (uint16_t)(a - b));
-
您可以在桌面上尝试 c++ 版本。使用 Int16 类,您可以重载算术运算符。
-
投到
uint16_t的好处只有这么多...... -
@PawełDymowski 不,使用
%u打印已签名的 int 仍然是 UB。因为有些平台的无符号值可能有陷阱位。 Which integral promotions do take place when printing a char?, What happens when I use the wrong format specifier?
标签: c gcc clang integer-promotion