目录
1.知识回顾
之前在E35.【C语言】判断大/小端序文章讲过大小端序判断方法,但讲得有点粗糙,本文将分析Redis项目的部分源码来看大小端序的转换问题
2.要用到的Redis源码
Github链接:https://github.com/redis/redis/tree/unstable/src
要分析的源代码文件:
(endiancov全称为endian convert,即端序转换)
如果Github访问不稳定或者无法访问,百度网盘下载链接:https://pan.baidu.com/s/1fo9XuVBDbqgS1JXkmkWewA?pwd=tkxe 提取码: tkxe
3.前置知识
snprintf函数
函数声明: int snprintf ( char * s, size_t n, const char * format, ... ); ,显然为不定参函数,作用和printf略有不同
作用
向已定大小的缓冲区(sized buffer)写入格式化(formatted)的缓冲区(buffer,其实是数组)
各个参数说明
s为char*类型的指针,指向要存储字符串的buffer数组,注意:buffer数组至少可以存n个字符
n:buffer数组中最大可以使用的字节数,字符串的大小最多n-1字节,因为字符串的结尾要填充\0,n-1+1==n,这样正好填满容量为n个字符的buffer数组
format:和printf函数的format一样,这里不再赘述
...:表示额外的参数,可有可无,是否有额外的参数取决于format指向的字符串,例如"%d",2中2为额外的参数,这一点和printf一样,这里也不再赘述
返回值
如果buffer数组有充足的空间,且成功写入字符串,则返回写入字符的个数(不含\0),成功写入时,返回值必须非负(non-negative)且小于n;如果出现编码错误,返回一个负数(通常是-1)
代码示例
正常返回:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char buffer[20];
int cx;
cx = snprintf(buffer, 20, "Hello %s","World!");
if (cx >= 0 && cx < 6)//检查返回值
puts(buffer);
else
{
printf("解码错误,snprintf返回值为:%d", cx);
exit(EXIT_FAILURE);
}
return 0;
}
运行结果:
变式训练1
修改上方代码为:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char buffer[20];
int cx;
cx = snprintf(buffer, 6, "Hello %s","World!");
if (cx >= 0 && cx < 6)//检查返回值
puts(buffer);
else
{
printf("解码错误,snprintf返回值为:%d", cx);
exit(EXIT_FAILURE);
}
return 0;
}
求运行结果
发现20改成了6,虽然buffer最多可以存储20个字符,但是buffer中最大可使用6个字节,则buffer存储的字符串为"Hello\0"
运行结果:
变式训练2
修改上方代码为:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char buffer[3];
int cx;
cx = snprintf(buffer, 6, "Hello %s","World!");
if (cx >= 0 && cx < 20) // check returned value
puts(buffer);
else
{
printf("解码错误,snprintf返回值为:%d", cx);
exit(EXIT_FAILURE);
}
return 0;
}
求运行结果
发现sprintf的第二个参数6小于buffer[3]的3,导致缓冲区溢出,在Dev C++上测试结果:
4.接口的使用
测试函数endianconvTest接口使用
在endianconv.c文件中有一个测试函数
#ifdef REDIS_TEST
#include <stdio.h>
#define UNUSED(x) (void)(x)
int endianconvTest(int argc, char *argv[], int flags) {
char buf[32];
UNUSED(argc);
UNUSED(argv);
UNUSED(flags);
snprintf(buf,sizeof(buf),"ciaoroma");//写入"ciaoroma"到buf数组
memrev16(buf);
printf("%s\n", buf);
snprintf(buf,sizeof(buf),"ciaoroma");
memrev32(buf);
printf("%s\n", buf);
snprintf(buf,sizeof(buf),"ciaoroma");
memrev64(buf);
printf("%s\n", buf);
return 0;
}
#endif
在96.【C语言】解析预处理(4)文章中讲过,如果想调用endianconvTest函数需要手动打开接口,即在#ifdef前面添加一行定义:
#define REDIS_TEST
再写一个main函数去调用endianconvTest函数接口
int main()
{
endianconvTest(0,NULL,0);//随意传参,函数内部并没有使用
return 0;
}
运行结果:
使用宏来调用
大致读c文件开头的注释
"This functions are never called directly, but always using the macros
defined into endianconv.h,..."表明该函数不会直接调用,而是通过定义在endianconv.h中的宏来使用
调用方法
准备工作
在endianconv.h中有#include "config.h",需要手动添加config.h
下载地址 https://github.com/redis/redis/blob/unstable/src/config.hz
直接编译会报错:
在#if !defined(BYTE_ORDER) || \ (BYTE_ORDER != BIG_ENDIAN && BYTE_ORDER != LITTLE_ENDIAN) =前添加字节序的一行定义即可,如下:
#define BYTE_ORDER BIG_ENDIAN
测试代码main.c
#include "endianconv.h"
#include <stdio.h>
int main()
{
char buffer[] = { "teststring" };
memrev16ifbe(buffer);
printf(buffer);
return 0;
}
运行结果:\只交换teststring的前两个字符的位置
分析:memrev16ifbe其实是宏
,由于定义BYTE_ORDER为BIG_ENDIAN,则 memrev16ifbe(buffer)会被替换为memrev16(buffer),转而去调用memrev16(相邻两字节交换位置)函数,注:((void)(0))其实是无操作(不做任何事)
其他函数接口测试
(交换了"teststring"的前4个字节)
(交换了"teststring"的前8个字节)
5.源码分析
memrev16
void memrev16(void *p) {
unsigned char *x = p, t;
t = x[0];
x[0] = x[1];
x[1] = t;
}
16代表16bit,即2个字节,临时指针变量x接收指针p的值,使用中间变量t来交换x[0]和x[1]存储的值
memrev32
void memrev32(void *p) {
unsigned char *x = p, t;
t = x[0];
x[0] = x[3];
x[3] = t;
t = x[1];
x[1] = x[2];
x[2] = t;
}
32代表32bit,即4个字节,互换位置
可以画图分析:
memrev64
void memrev64(void *p) {
unsigned char *x = p, t;
t = x[0];
x[0] = x[7];
x[7] = t;
t = x[1];
x[1] = x[6];
x[6] = t;
t = x[2];
x[2] = x[5];
x[5] = t;
t = x[3];
x[3] = x[4];
x[4] = t;
64代表64bit,即8个字节,互换位置