当前位置: 首页 > 知识库问答 >
问题:

C/C中未签名左移前的掩蔽是否太偏执?

邓才
2023-03-14

这个问题的动机是我在C/C中实现加密算法(例如SHA-1),编写与平台无关的可移植代码,并彻底避免未定义的行为。

假设一个标准化的加密算法要求您实现:

b = (a << 31) & 0xFFFFFFFF

其中ab是无符号32位整数。请注意,在结果中,我们丢弃了最低有效32位以上的任何位。

作为第一个简单的近似值,我们可以假设在大多数平台上,int的宽度为32位,因此我们将编写:

unsigned int a = (...);
unsigned int b = a << 31;

我们知道这段代码不会在任何地方都有效,因为在某些系统上,int是16位宽,在其他系统上是64位宽,甚至可能是36位宽。但是使用stdint。h,我们可以使用uint32\u t类型改进此代码:

uint32_t a = (...);
uint32_t b = a << 31;

我们结束了,对吗?这就是我多年来的想法。。。不完全是。假设在某个平台上,我们有:

// stdint.h
typedef unsigned short uint32_t;

在C/C中执行算术操作的规则是,如果类型(例如)比int窄,那么如果所有值都可以匹配,则将其扩展为int,否则将扩展为un符号int

假设编译器将定义为32位(有符号),将int定义为48位(有符号)。然后这些代码行:

uint32_t a = (...);
uint32_t b = a << 31;

将有效地意味着:

unsigned short a = (...);
unsigned short b = (unsigned short)((int)a << 31);

请注意,a被提升为int,因为所有ushort(即uint32)都适合int(即int48)。

但是现在我们有一个问题:将非零位向左移动到有符号整数类型的符号位是未定义的行为。这个问题的发生是因为我们的uint32被提升到int48-而不是提升到uint48(在那里左移是可以的)。

以下是我的问题:

>

  • 我的推理正确吗,这在理论上是一个合理的问题吗?

    因为在每个平台上,下一个整数类型都是宽度的两倍,所以忽略这个问题安全吗?

    通过像这样预先掩蔽输入来正确防御这种病理情况是一个好主意吗<代码>b=(a)

    澄清/编辑:

    >

  • 我接受C或C或两者的答案。我想知道至少一种语言的答案。

    预掩蔽逻辑可能会影响位旋转。例如,GCC将编译b=(a


  • 共有3个答案

    丌官浩旷
    2023-03-14

    从这个关于uint32*uint32算术中可能的UB的问题中获得线索,以下简单的方法应该适用于C和C:

    uint32_t a = (...);
    uint32_t b = (uint32_t)((a + 0u) << 31);
    

    整数常量0u的类型为无符号int。这会将添加a 0u提升到uint32_t无符号int,以较宽者为准。因为该类型的级别为int或更高,所以不会发生更多的提升,并且可以在左操作数为uint32_t无符号int的情况下应用移位。

    最后一次返回到uint32_t只会抑制关于变窄转换的潜在警告(比如int为64位)。

    一个好的C编译器应该能够看到添加零是无操作的,这比看到前置掩码在无符号转移后没有效果要简单得多。

    法烨烨
    2023-03-14

    问题1:轮班前的掩蔽确实可以防止OP担心的未定义行为。

    问题2:“……因为在每个平台上,下一个整数类型都是宽度的两倍?”--

    对于所有具有uint32\u t的兼容C编译器,以下内容都有很好的定义。

    uint32_t a; 
    uint32_t b = (a & 1) << 31;
    

    Q3:<代码>uint32_t;uint32_tb=(a

    正如建议的那样,最好强调这些变化的无符号性。

    uint32_t b = (a & 1U) << 31;
    

    @John Bollinger好答案详细说明了如何处理OP的具体问题。

    通常的问题是如何形成一个至少有n位,具有一定符号性且不会受到令人惊讶的整数提升的数字——这是OP困境的核心。下面通过调用一个不改变值的无符号操作来实现这一点——除了类型问题之外,这是一个有效的无操作。乘积至少是无符号uint32_t的宽度。一般来说,强制转换可能会缩小类型。除非强制不会出现强制转换,否则需要避免强制转换。优化编译器不会创建不必要的代码。

    uint32_t a;
    uint32_t b = (a + 0u) << 31;
    uint32_t b = (a*1u) << 31;
    
    西门经国
    2023-03-14

    说到问题的C面,

    1. 我的推理正确吗,这在理论上是一个合理的问题吗?

    是我之前没有考虑过的问题,但是我同意你的分析。C定义了的行为

    C不需要整数类型之间的这种关系,尽管它在实践中无处不在。然而,如果您决心只依赖标准——也就是说,如果您不厌其烦地编写严格符合标准的代码——那么您就不能依赖这种关系。

    类型无符号长保证至少有32个值位,并且在整数提升下不会被提升到任何其他类型。在许多常见平台上,它与uint32_t具有完全相同的表示形式,甚至可能是相同的类型。因此,我倾向于这样写表达式:

    uint32_t a = (...);
    uint32_t b = (unsigned long) a << 31;
    

    或者,如果您只需要a作为b计算中的中间值,则首先将其声明为无符号长

     类似资料:
    • 我不确定安德烈·卡隆在这里是什么意思: C中的虚拟函数 ...其中一些代码依赖于(正式的)非标准行为,这些行为“恰好”在大多数编译器上工作。主要问题是代码假设 < code>m的类型为< code>struct meh *。类型为< code>struct foo *的对象f通过向< code>struct meh *的强制转换被赋给m。< code>struct meh具有< code>stru

    • 本文向大家介绍C#中实现屏蔽Ctrl+C的方法,包括了C#中实现屏蔽Ctrl+C的方法的使用技巧和注意事项,需要的朋友参考一下 本文实例讲述了C#实现屏蔽Ctrl+C的方法,代码简单易懂,具有一定的实用价值。分享给大家供大家参考。具体方法如下: 主要实现方法为重写 WndProc,代码如下: 希望本文所述C#实例对大家有所帮助。

    • 我需要用c语言验证由JAVA签名API生成的签名。我有一套相同的公钥和私钥。我还验证了c语言的签名和验证工作。但是我在验证JAVA生成的签名时遇到了问题。我看了所有的文件,尝试了不同的方法,但我似乎还是不明白。我将使用用于验证的原始JAVA代码粘贴等效的c代码。 原始JAVA代码: C代码: 我有3个问题: 这是验证签名的正确方法吗? 我需要在验证数据之前对其进行散列吗?如果是,那么JAVA在签名

    • 主要内容:基类成员函数和派生类成员函数不构成重载如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。 下面是一个成员函数的名字遮蔽的例子: 运行结果: 小明的年龄是16,成绩是90.5 嗨,大家好,我叫小明,今年16岁 本例中,基类 People 和派

    • 我有一个使用C++中的openSSL生成的keypair,我正在使用它在一个严格使用RSACryptoServiceProvider(没有BouncyCastle等)的C#服务器上对验证消息进行签名。我使用PKCS#1 SHA256生成签名,然后以十六进制的形式与公钥一起传输签名。问题是无法在服务器上验证签名。我已经尝试删除了头,上面写着“----开始RSA公钥-----”等等。但是还没有结果。生

    • 下面是C 17形式的规则([basic.lval]/8),但它在其他标准中看起来很相似(C 98中是“lvalue”而不是“glvalue”): 8如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义: (8.4)-对应于对象动态类型的有符号或无符号类型 这条规则听起来像是“除非你做X,否则你会得到UB”,但这并不意味着如果你做了X,你就不会得到UB,正如人们所期望的那样