2016
May
08

网页好读版

公司 OS 要全面升级至 64 Bit 环境,其中包含了超过 20 个机器群组,一时间无法做完,只好先将部分机器升上 64 Bit ,其它旧的机器就保留为 32 Bit ,这代表有些 32 Bit 机器会传资料给 64 Bit,而问题就来了, 32 Bit 的变数长度跟 64 Bit 是不同的,例如我在 32 Bit 中写入一个 size_t 4 Bytes 长度的数字, 再把这个变数传给 64 Bit Server ,这时 Server 会试著去读取一个 8 Bytes 长度的数字,结果取得的资料就跟 32 Bit 传给来的值不同。

基本变数长度

以下是各种变数型态的长度

Variable 32 Bit (bytes) 64 Bit (bytes)
short 2 2
float 4 4
int 4 4
double 8 8
long 4 8
long long 8 8
size_t 4 8

为了 32/64 Bit 相容性,第一件事就是将 int 修改成 int32_t ,虽然 32/64 Bit 的环境中宣告的 int 长度是一致的,不过为了容易了解,我还是将 int 改成 int32_t ,再将 size_t 与 long 改成 int64_t。

int32_t 与 int64_t 代表强迫指定 int 的长度 , int32_t 的长度为 32 Bit, 4 Bytes , int64_t 的长度为 64 Bit , 8 Bytes 。

修改完后,两个境环的变数长度就一致了,而且这次的修改 ,不用重新编译 64 Bit 的环境,因为 size_t 改成 int64_t ,对 64 Bit 的环境来说,变数的长度是一样的,所以就算不重新编译也不会有问题。

Example
  1. #include <stdio.h>
  2. #include <inttypes.h>
  3.  
  4. int => int32_t a;
  5. size_t => int64_t b;

Struct 长度

在解决了 integer 长度之后,本来想说就搞定下班了,但是没想到,程式还是有问题, client 传给 server 的资料还是有错, server 仍然出现 segmentation fault ,追查了一阵子,发现 struct 的长度有点问题, client 传出来的 struct 长度为 12 Bytes ,而 server 要接收的 struct 长度为 16 Bytes,两个环境对 struct 的长度定义不同。

程式中 struct 的结构如下:

struct
  1. struct resHeader {
  2. static int code;
  3. int status;
  4. unsigned long long length;
  5. };

这个 struct 中有一个 static int ,这种静态变数是不会增加 struct 的长度, static 在程序中只能存在一个,每个 struct resHeader 都会取存到同一个 static 数值,所以 struct resHeader 中只有 int status 与 unsigned long long length 会影响到 resHeader 的长度, int 的长度在 32/64 Bit 环境中都是 4 bytes ,而 long long 的长度在 32/64 Bit 环境中也都是 8 bytes, 两个总合起来,struct resHeader 的长度为 12 bytes。

但是我实际测试的结果, struct resHeader 在 64 Bit 环境中的长度是 16 bytes,下面这段是我的测试程式。

64 Bit struct
  1. #include <stdio.h>
  2. struct resHeader {
  3. static int code;
  4. int status;
  5. unsigned long long length;
  6. };
  7. int main() {
  8. printf("size = %d", sizeof(resHeader));
  9. }
  10. // g++ size.cc && ./a.out
  11. // size = 16

追查了很久之后,才知道原来 64 Bit 编译器,为了优化 Performance ,每次读取 memory 的时候,一定要读一个固定 bytes 的倍数,也就是一次读取 8 bytes, 16 bytes, 24 bytes,所以 struct 的长度也会被自动优化成某一个值的倍数。

首先 Compiler 会先在这个 struct resHeader 中找一个需要最多 memory 的基本变数,在我的例子中 long long 会最大的变数值,它需要的长度为 8 bytes ,所以这个 struct 中的所有变数后面都会被自动补上 Null 凑成 8 bytes 的倍数, 例如 status 为 int 需要 4 个 bytes ,后面会被接上 4 个 Null,再来 length 为 long long 需要 8 bytes, 8 bytes 就是 8 的倍数,所以不用补 Null ,最后在 64-Bit 环境中 ,这个 structure 就会变成 16 bytes。

64-bit
  1. struct resHeader {
  2. static int code; // static
  3. int status; // Memory 位置 0 - 3
  4. char pad[4]; // 自动补上 4 个 null, Memory 位置 4 - 7
  5. unsigned long long length; // Memory 位置 8 - 15
  6. // Total 16 bytes
  7. };

那么 32-bit 的环境中这个 struct memory address 会是怎样的呢? 同上的逻辑,Compiler 先找到一个最大的变数 length(8 bytes) ,但是 32-bit 的环境中一次读取的最大 memory 为 4 bytes (32 bits) , 8 bytes 已经超过这个值,所以 Compiler 会将最大的 memory 读取自动调整为 4 bytes ,代表每个变数只要是 4 的倍数即可。

32-bit
  1. struct resHeader {
  2. static int code; // static
  3. int status; // Memory 位置 0 - 3 , 刚好为 4 的倍数,不用补 Null
  4. unsigned long long length; // Memory 位置 4 - 11
  5. // Total 12 bytes
  6. };

32-Bit 环境的 struct 长度是 12 bytes ,而 64-Bit 的环境被自动补上了 4 bytes 的 null 形成 16 bytes,这个问题就是造成 32/64 Bit 环境不相容的原因。

解决方法也不难,上述两个情形的最大公倍数为 8 ,所以我们只要把 struct 的长度弄成 8 bytes 的倍数,然后将 C/C++ 重新 compile ,分别装到 client/server 的机器就搞定收工罗。

Example
  1. struct resHeader {
  2. static int code;
  3. int64_t status;
  4. int64_t length;
  5. };

No Downtime

以上的解法,还有个小问题, 因为 server 目前已经有两组机器,一组是 32 Bit ,一组是 64 Bit 如果要将 client 机器装上新版相容 64 Bit 的程式,那么代表 64 Bit 的 server 也要一并装上新版的程式,而且还要同时 restart 伺服器,但是我们公司的机器数可不是普通的多,一时半刻是无法将全部的机器安装完毕的,再加上公司平常 Release 都是追求 No Downtime 的目标在做,所以再来要想想是否能够单独安装新的 package 到 client 的机器,而且不影响线上的运作。

第一步我先来测试旧的 struct resHeader 在 64 Bit 环境中,struct resHeader memory 储存的格式是什么, g++ old_struct.cc && ./a.out

old_struct.cc
  1. #include <stdio.h>
  2. #include <inttypes.h>
  3. struct resHeader {
  4. static int code;
  5. int status;
  6. unsigned long long length;
  7. };
  8. int main() {
  9. struct resHeader k = {2,4};
  10. unsigned char *p = (unsigned char*) &k;
  11. for (int i = 0; i < sizeof(struct resHeader); i++) {
  12. printf("%02x ", p[i]);
  13. }
  14. return 1;
  15. }

将 memory 印出来之后如下,前 4 个 bytes 存的是 status,接著会有 4 个 bytes 的 null 值 ,后 8 个 bytes 存的是 length,刚好加起来是 16 bytes 。

02 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00

这个格式看起来跟我们改的 struct resHeader 是一致的, 来测试一下新改的 struct 在 32 Bit 环境下的执行结果, g++ -m32 new_struct.cc && ./a.out

new_struct.cc
  1. #include <stdio.h>
  2. #include <inttypes.h>
  3.  
  4. struct resHeader {
  5. static int code;
  6. int64_t status;
  7. int64_t length;
  8. };
  9. int main() {
  10. struct resHeader k = {2,4};
  11. unsigned char *p = (unsigned char*) &k;
  12. for (int i = 0; i < sizeof(struct resHeader); i++) {
  13. printf("%02x ", p[i]);
  14. }
  15. return 1;
  16. }

执行结果如下,memory 存的方式跟旧程式在 64 Bit 上执行的结果一样,这代表了我只要将新程式安装到 client 的机器上, client (32 Bit) 与 server (64 Bit) 的机器就能相容了。

02 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00

最后实际在 Beta 环境上测试成功,终於可以下班了...

参考资料

网页好读版