返回介绍

18.6 结构体中的位

发布于 2025-02-22 14:00:47 字数 8276 浏览 0 评论 0 收藏 0

18.6.1 CPUID 的例子

C/C++中允许给结构体的每一个成员都定义一个准确的位域。如果我们想要节省空间的话,这个对我们来说将是非常有用的。比如,对 BOOL 来说,1 位就足矣了。但是当然,如果我们想要速度的话,必然会浪费点空间。 让我们以 CPUID 指令为例,这个指令返回当前 CPU 的信息和特性。 如果 EAX 在指令执行之前就设置为了 1,CPUID 将会返回这些内容到 EAX 中。

enter image description here

MSVC 2010 有 CPUID 的宏,但是 GCC 4.4.1 没有,所以,我们就手动的利用它的内联汇编器为 GCC 写一个吧。

#!cpp
#include <stdio.h>
#ifdef __GNUC__
static inline void cpuid(int code, int *a, int *b, int *c, int *d) {
asm volatile("cpuid":"=a"(*a),"=b"(*b),"=c"(*c),"=d"(*d):"a"(code));
}
#endif
#ifdef _MSC_VER
#include <intrin.h>
#endif
struct CPUID_1_EAX
{
    unsigned int stepping:4;
    unsigned int model:4;
    unsigned int family_id:4;
    unsigned int processor_type:2;
    unsigned int reserved1:2;
    unsigned int extended_model_id:4;
    unsigned int extended_family_id:8;
    unsigned int reserved2:4;
};
int main()
{
    struct CPUID_1_EAX *tmp;
    int b[4];
    #ifdef _MSC_VER
    __cpuid(b,1);
    #endif
    #ifdef __GNUC__
    cpuid (1, &b[0], &b[1], &b[2], &b[3]);
    #endif
    tmp=(struct CPUID_1_EAX *)&b[0];
    printf ("stepping=%d
", tmp->stepping);
    printf ("model=%d
", tmp->model);
    printf ("family_id=%d
", tmp->family_id);
    printf ("processor_type=%d
", tmp->processor_type);
    printf ("extended_model_id=%d
", tmp->extended_model_id);
    printf ("extended_family_id=%d
", tmp->extended_family_id);
    return 0;
};

之后 CPU 会填充 EAX,EBX,ECX,EDX,这些寄存器的值会通过 b[]数组显现出来。接着我们用一个指向 CPUID_1_EAX 结构体的指针,把它指向 b[]数组的 EAX 值。 换句话说,我们将把 32 位的 INT 类型的值当作一个结构体来看。 然后我们就能从结构体中读取数据。 让我们在 MSVC 2008 用/Ox 编译一下:

清单 18.19: MSVC 2008

#!bash
_b$ = -16 ; size = 16
_main PROC
    sub esp, 16 ; 00000010H
    push ebx
    xor ecx, ecx
    mov eax, 1
    cpuid
    push esi
    lea esi, DWORD PTR _b$[esp+24]
    mov DWORD PTR [esi], eax
    mov DWORD PTR [esi+4], ebx
    mov DWORD PTR [esi+8], ecx
    mov DWORD PTR [esi+12], edx
    mov esi, DWORD PTR _b$[esp+24]
    mov eax, esi
    and eax, 15 ; 0000000fH
    push eax
    push OFFSET $SG15435 ; ’stepping=%d’, 0aH, 00H
    call _printf
    mov ecx, esi
    shr ecx, 4
    and ecx, 15 ; 0000000fH
    push ecx
    push OFFSET $SG15436 ; ’model=%d’, 0aH, 00H
    call _printf
    mov edx, esi
    shr edx, 8
    and edx, 15 ; 0000000fH
    push edx
    push OFFSET $SG15437 ; ’family_id=%d’, 0aH, 00H
    call _printf
    mov eax, esi
    shr eax, 12 ; 0000000cH
    and eax, 3
    push eax
    push OFFSET $SG15438 ; ’processor_type=%d’, 0aH, 00H
    call _printf
    mov ecx, esi
    shr ecx, 16 ; 00000010H
    and ecx, 15 ; 0000000fH
    push ecx
    push OFFSET $SG15439 ; ’extended_model_id=%d’, 0aH, 00H
    call _printf
    shr esi, 20 ; 00000014H
    and esi, 255 ; 000000ffH
    push esi
    push OFFSET $SG15440 ; ’extended_family_id=%d’, 0aH, 00H
    call _printf
    add esp, 48 ; 00000030H
    pop esi
    xor eax, eax
    pop ebx
    add esp, 16 ; 00000010H
    ret 0
_main ENDP

SHR 指令将 EAX 寄存器的值右移位,移出去的值必须被忽略,例如我们会忽略右边的位。 AND 指令将清除左边不需要的位,换句话说,它处理过后 EAX 将只留下我们需要的值。 让我们在 GCC4.4.1 下用-O3 编译。

清单 18.20: GCC 4.4.1

#!cpp
main proc near ; DATA XREF: _start+17
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    push esi
    mov esi, 1
    push ebx
    mov eax, esi
    sub esp, 18h
    cpuid
    mov esi, eax
    and eax, 0Fh
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aSteppingD ; "stepping=%d
"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov eax, esi
    shr eax, 4
    and eax, 0Fh
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aModelD ; "model=%d
"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov eax, esi
    shr eax, 8
    and eax, 0Fh
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aFamily_idD ; "family_id=%d
"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov eax, esi
    shr eax, 0Ch
    and eax, 3
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aProcessor_type ; "processor_type=%d
"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov eax, esi
    shr eax, 10h
    shr esi, 14h
    and eax, 0Fh
    and esi, 0FFh
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aExtended_model ; "extended_model_id=%d
"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov [esp+8], esi
    mov dword ptr [esp+4], offset unk_80486D0
    mov dword ptr [esp], 1
    call ___printf_chk
    add esp, 18h
    xor eax, eax
    pop ebx
    pop esi
    mov esp, ebp
    pop ebp
    retn
main endp

几乎一样。只有一个需要注意的地方就是 GCC 在调用每个 printf() 之前会把 extended_model_id 和 extended_family_id 的计算联合到一块去,而不是把它们分开计算。

18.6.2 将浮点数当作结构体看待

我们已经在 FPU(15 章)中注意到了 float 和 double 两个类型都是有符号的,他们分为符号、有效数字和指数部分。但是我们能直接用上这些位嘛?让我们试一试 float。

enter image description here

#!cpp
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <memory.h>
struct float_as_struct
{
    unsigned int fraction : 23; // fractional part
    unsigned int exponent : 8; // exponent + 0x3FF
    unsigned int sign : 1; // sign bit
};
float f(float _in)
{
    float f=_in;
    struct float_as_struct t;
    assert (sizeof (struct float_as_struct) == sizeof (float));
    memcpy (&t, &f, sizeof (float));
    t.sign=1; // set negative sign
    t.exponent=t.exponent+2; // multiple d by 2^n (n here is 2)
    memcpy (&f, &t, sizeof (float));
    return f;
};
int main()
{
    printf ("%f
", f(1.234));
};

float_as_struct 结构占用了和 float 一样多的内存空间,也就是 4 字节,或者说,32 位。 现在我们给输入值设置一个负值,然后指数加 2,这样我们就能把整个数按照 22 的值来倍乘,也就是乘以 4。 让我们在 MSVC2008 无优化模式下编译它。

清单 18.21: MSVC 2008

#!bash
_t$ = -8 ; size = 4
_f$ = -4 ; size = 4
__in$ = 8 ; size = 4
?f@@YAMM@Z PROC ; f
    push ebp
    mov ebp, esp
    sub esp, 8
    fld DWORD PTR __in$[ebp]
    fstp DWORD PTR _f$[ebp]
    push 4
    lea eax, DWORD PTR _f$[ebp]
    push eax
    lea ecx, DWORD PTR _t$[ebp]
    push ecx
    call _memcpy
    add esp, 12 ; 0000000cH
    mov edx, DWORD PTR _t$[ebp]
    or edx, -2147483648 ; 80000000H - set minus sign
    mov DWORD PTR _t$[ebp], edx
    mov eax, DWORD PTR _t$[ebp]
    shr eax, 23 ; 00000017H - drop significand
    and eax, 255 ; 000000ffH - leave here only exponent
    add eax, 2 ; add 2 to it
    and eax, 255 ; 000000ffH
    shl eax, 23 ; 00000017H - shift result to place of bits 30:23
    mov ecx, DWORD PTR _t$[ebp]
    and ecx, -2139095041 ; 807fffffH - drop exponent
    or ecx, eax ; add original value without exponent with new calculated exponent
    mov DWORD PTR _t$[ebp], ecx
    push 4
    lea edx, DWORD PTR _t$[ebp]
    push edx
    lea eax, DWORD PTR _f$[ebp]
    push eax
    call _memcpy
    add esp, 12 ; 0000000cH
    fld DWORD PTR _f$[ebp]
    mov esp, ebp
    pop ebp
    ret 0
?f@@YAMM@Z ENDP ; f

有点多余。如果用/Ox 编译的话,这里就没有 memcpy 调用了。f 变量会被直接使用,但是没有优化的版本看起来会更容易理解一点。 GCC 4.4.1 的-O3 选项会怎么做?

清单 18.22: Gcc 4.4.1

#!bash
; f(float)
public _Z1ff
_Z1ff proc near
var_4 = dword ptr -4
arg_0 = dword ptr 8
    push ebp
    mov ebp, esp
    sub esp, 4
    mov eax, [ebp+arg_0]
    or eax, 80000000h ; set minus sign
    mov edx, eax
    and eax, 807FFFFFh ; leave only significand and exponent in EAX
    shr edx, 23 ; prepare exponent
    add edx, 2 ; add 2
    movzx edx, dl ; clear all bits except 7:0 in EAX
    shl edx, 23 ; shift new calculated exponent to its place
    or eax, edx ; add new exponent and original value without exponent
    mov [ebp+var_4], eax
    fld [ebp+var_4]
    leave
    retn
    _Z1ff endp
    public main
    main proc near
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 10h
    fld ds:dword_8048614 ; -4.936
    fstp qword ptr [esp+8]
    mov dword ptr [esp+4], offset asc_8048610 ; "%f
"
    mov dword ptr [esp], 1
    call ___printf_chk
    xor eax, eax
    leave
    retn
main endp

F()函数基本可以理解,但是有趣的是,GCC 可以在编译阶段就通过我们这堆大杂烩一样的代码计算出 f(1.234) 的值,从而会把他当作参数直接给 printf()。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文