c/c++强制类型转换在实际项目中可能存在的风险(大端法、小端法)

299 阅读1分钟

今天在阅读项目代码的时候看到这样一段代码:

//信号处理函数
void Utils::sig_handler(int sig)
{
    //为保证函数的可重入性,保留原来的errno
    int save_errno = errno;
    int msg = sig;
    send(u_pipefd[1], (char *)&msg, 1, 0);      
    errno = save_errno;
}

这是一个普通的信号处理函数,用于指定信号sig触发时的行为。即向管道u_pipefd[1]发送该信号本身。

当管道的另一端u_pipefd[0]接收到信号时,处理的逻辑如下

bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{
    int ret = 0;
    int sig;
    char signals[1024];
    ret = recv(m_pipefd[0], signals, sizeof(signals), 0);
    if (ret == -1){
        return false;
    }
    else if (ret == 0){
        return false;
    }
    else{
        for (int i = 0; i < ret; ++i){
            switch (signals[i]){
                case SIGALRM:{
                    timeout = true;
                    break;
                }
                case SIGTERM:{
                    stop_server = true;
                    break;
                }
            }
        }
    }
    return true;
}

先忽略代码其他不重要的逻辑,重点看ret = recv(m_pipefd[0], signals, sizeof(signals), 0);。该函数首先将信号从管道中读取出来,然根据取出的值进行相应的逻辑处理

switch (signals[i]) { 
    case SIGALRM: { 
        timeout = true; break; 
    } 
    case SIGTERM: { 
        stop_server = true; break; 
    } 
}

具体来说,项目中注册了SIGALRM和SIGTERM,他们对应的值和含义如下: image.png

回到标题,看到这段代码时,我产生了第一个疑问:

为什么只发送一个字节?

  • 因为信号是通过一个整数编号来标识的。这个编号通常很小,一个字节(8位)足够表示,所以能发一个字节就别发4个字节了。

为什么要将&msg强制转换为char*,不转换行不行?

  • 先说答案:可以不转,但是和原本的代码一样存在风险(大小端问题)
  • 强制类型转换改变了什么?
    • 从数据的表示来说,什么也没有改变
    • 它仅仅改变了数据解释的方式
  • 从下面的实例代码一目了然
# include <stdio.h>

void show_bytes(char * start, size_t len) {
    size_t i;
    for (i = 0; i < len; i++) 
        printf(" %.2x", start[i]);
    printf("\n");
}

int main() {
    int sig = 15;
    show_bytes((char *)&sig, sizeof(int));
    return 0;
}

//输出
0f 00 00 00

由于我的机器为小端法(数据的低字节存储在存储器地址的低位,数据的高字节存储在存储器地址的高位),所以将源程序改为下面的形式也是一模一样的。

//信号处理函数
void Utils::sig_handler(int sig)
{
    //为保证函数的可重入性,保留原来的errno
    int save_errno = errno;
    send(u_pipefd[1], &sig, 1, 0);      
    errno = save_errno;
}

不管转不转换,发送到管道中的都是0x0f这8个比特的数据

问题出在哪里?

  • 如前所述,如果机器采用小端法表示数据,就没有任何问题,但是如果机器采用大端法表示,就会出现严重的问题。
  • 倘若为大端法表示(数据的低字节存储在存储器地址的高位,数据的高字节存储在存储器地址的地位),SIGTERM(15)在机器中的存储顺序为:00 00 00 0f
  • 不管转不转换,发送到管道中的都是0x00这8个比特的数据

如何解决?

将代码改为下面,即可解决

//信号处理函数
void Utils::sig_handler(int sig)
{
    //为保证函数的可重入性,保留原来的errno
    int save_errno = errno;
    char msg = static_cast<char>(sig); 
    send(u_pipefd[1], &msg, 1, 0); 
    errno = save_errno;
}

最后一点

强制类型转换不会改变数据在机器上的表示,只会改变对数据的解释。 例如SIGTERM(15)在机器上的表示为0x0f,如果用int 来解释就是15,如果用char来解释就是ASCII码表中的一个字符

所以信号处理逻辑部分和下面是等价的:

switch (signals[i]) { 
    case 0x0e: { 
        timeout = true; break; 
    } 
    case 0x0f: { 
        stop_server = true; break; 
    } 
}

当然写成宏定义的形式更直观,这里只是为了便于理解。