奇怪的全局变量行为,一旦更改变量名,问题就会消失

Posted

技术标签:

【中文标题】奇怪的全局变量行为,一旦更改变量名,问题就会消失【英文标题】:Strange global variable behaviour, once variable name is changed issue disappears 【发布时间】:2012-04-08 01:24:10 【问题描述】:

在我的大学练习中,我遇到了一个变量的奇怪行为。

/* Main parameters                                                          */
double sizeX, sizeY;      /* Size of the global domain                      */
int nPartX, nPartY;       /* Particle number in x, y direction              */
int nPart;                /* Total number of particles                      */
int nCellX, nCellY;       /* (Global) number of cells in x, y direction     */
int steps;                /* Number of timesteps                            */
double dt;                /* Stepsize for timesteps                         */
int logs;                 /* Whether or not we want to keep logfiles        */

void ReadInput(const char *fname)

  FILE *fp;
  char c;

  Debug("ReadInput", 0);
  if(rank == 0)
  
    fp = fopen(fname, "r");
    if(!fp) Debug("Cannot open input file", 1);
    if(fscanf(fp, "sizeX: %lf\n", &sizeX) != 1) Debug("sizeX?",  1);
    if(fscanf(fp, "sizeY: %lf\n", &sizeY) != 1) Debug("sizeY?",  1);
    if(fscanf(fp, "nPartX:%i\n", &nPartX) != 1) Debug("nPartX?", 1);
    if(fscanf(fp, "nPartY:%i\n", &nPartY) != 1) Debug("nPartY?", 1);
    if(fscanf(fp, "nCellX:%i\n", &nCellX) != 1) Debug("nCellX?", 1); //read value is 10
    if(fscanf(fp, "nCellY:%i\n", &nCellY) != 1) Debug("nCellY?", 1);    
    if(fscanf(fp, "steps: %li\n", &steps) != 1) Debug("steps?",  1);    
//here the nCellX variable value 10 is changed somehow to 0
    if(fscanf(fp, "dt:    %lf\n", &dt)    != 1) Debug("dt?",     1);
    if(fscanf(fp, "logs:  %c\n",  &c)     != 1) Debug("logs?",   1);
    logs = (c == 'y');
    fclose(fp);
  

  printf("(%i) reporting in...\n", rank);

  MPI_Bcast(&sizeX, 1, MPI_DOUBLE, 0, grid_comm);  
  MPI_Bcast(&sizeY, 1, MPI_DOUBLE, 0, grid_comm);
  MPI_Bcast(&nPartX,1, MPI_INT,    0, grid_comm);  
  MPI_Bcast(&nPartY,1, MPI_INT,    0, grid_comm);
  MPI_Bcast(&nCellX,1, MPI_INT,    0, grid_comm);
  MPI_Bcast(&nCellY,1, MPI_INT,    0, grid_comm);
  MPI_Bcast(&steps, 1, MPI_INT,    0, grid_comm);
  MPI_Bcast(&dt,    1, MPI_DOUBLE, 0, grid_comm);
  MPI_Bcast(&logs,  1, MPI_INT,    0, grid_comm);
  nPart = nPartX * nPartY;
  dt2 = dt * dt;

老师和我得出的结论是,如果我们将变量名从“nCellX”更改为“nCellX_2”,问题就消失了,代码按预期工作。另一个有趣的事情是,只有这个单一的全局变量有这个问题,其他变量都可以正常工作。我想知道是否有人也遇到过这种类型的问题。任何指导/解释将不胜感激。

如果这个问题不够清楚,请告诉我,如果需要完整代码,我也可以提供。通常,代码是 Particle-in-Cell 的并行算法。

【问题讨论】:

你的程序是多线程的吗? DEBUG 的代码是什么? 您是否收到任何编译器警告?尝试使用-Wall -Wextra -pedantic 进行编译,看看是否有任何收获。 另外,尝试通过将nCellX 设置为某个初始值来定义它。如果该名称之前已初始化(例如,在某处的 MPI 中),则会导致错误 【参考方案1】:

以下代码行可能导致问题:

if(fscanf(fp, "steps: %li\n", &steps) != 1) Debug("steps?",  1);

%li 表示一个长整数,可能是 64 位,而 steps 是一个 int,可能是 32 位。格式说明符应为%i 而不是%li

是否存在实际问题取决于环境(例如,如果构建 64 位应用程序,这很可能是一个问题)。如果存在 64 位与 32 位不匹配的情况,则 fscanf 调用将覆盖内存,并可能破坏内存布局中 steps 后面的任何变量(可能是 nCellX)。请注意,使用 -Wall 选项应该警告您这种情况。为什么将 nCellX 的名称更改为不同的名称应该掩盖问题尚不清楚,但似乎更改名称可能会导致内存中变量的布局发生变化;我怀疑 C 标准不允许这样做(尽管我没有看过)。

【讨论】:

这无法解释为什么将名称 nCellX 更改为 nCellX_2 可以解决问题。 @Shahbaz,是的,它可以。如果存在“未定义行为”的类型不匹配,那么任何事情都可能发生,即使是这样。 “任何事情都可能发生”,但编译器是确定性的。理论上是的,任何事情都可能发生,但实际上使用相同的编译器,相同的代码,除了一个变量名更改,输出是“相同的任何东西”,无论任何东西可能是什么。 在这种特殊情况下,如果 %listeps 被覆盖之前导致了变量,那么该变量的名称是什么并不重要。 @Shahbaz:更改变量名可以更改它在符号表中的出现位置以及相对于缓冲区溢出它在内存中的位置。它可以很容易地解释所报告的行为。【参考方案2】:

作为对@Mark Wilkins & Co. 评论的确认。我试图展示该命名 肯定会产生影响。

关于案例:fprintf() 使用一个指针来存储它读取的内容。它不知道 它指向的类型,但从格式中获取定义并强制转换 争论。类似sscanf("36", "%i", &my_dest); -> number = va_arg(vl, int*);

为你的编译器使用正确的标志来捕捉这个


当 exec 启动程序时,它通常会为未初始化的程序分配地址 数据(即 int foo;)在称为 BSS 的区域中。 (见下图1)。

在许多系统上,这将来自低内存地址及以上。

为了演示(在给定系统上)发生了什么,我们如下:

我从以下开始:

/* global scope */
unsigned char unA;
unsigned char unB;
unsigned char unC;
unsigned int  unD;

列表 1

main() 我说:

unA = '1';
unB = '2';
unC = '3';
/* bit shifting the "string" NAC! into unD, reverse order as my system is LSB 
 * first (little-endian), unD becomes 558055758 => by byte ASCII !CNA */
unD = 0 | ('!' << 24) | ('C' << 16) | ('A' << 8) | 'N';

清单 2

并将无符号字符指针指向unA 并转储以下 16 个字节 结果:转储格式为 [char],或带有前导零的十六进制(%c. 或 %02x)*

 +-- Address of unA
 |
0x804b06c: 1.3.0000N.A.C.!. 2.00000000000000 
           | |     |_____|  |
           | |        |     +--- unB
           | |        +--------- unD
           | +------------------ unC
           +-------------------- unA

列表 3

然后我将unB的名称更改为un2,文件中的顺序相同:

unsigned char unA;
unsigned char un2;
unsigned char unC;
unsigned int  unD;

列表 4

现在我的转储给出:

 +-- Address of unA
 |
0x804b06c: 1.3.2.00N.A.C.!. 0000000000000000
           | | |   |_____|  
           | | |      +--------- unD
           | | +---------------- unB
           | +------------------ unC
           +-------------------- unA

列表 5

可以看到地址/对齐的顺序已更改。 类型没有变化,只有名称。


分配错误的类型:

下一步是强制类型转换和溢出范围。 将un2 改回unB。 我们有 List 3 中的对齐方式。

我们创建了一个设置字节的函数(在具有 4 字节/32 位 int 的系统上), 高阶为:

void set_what(unsigned int *n)

    *n = 0 | ('t' << 24) | ('a' << 16) | ('h' << 8) | 'w';
    /* or *n = 0x74616877; in an ASCII environment 
     * 0x74 0x61 0x68 0x77 == tahw */

清单 6

main() 我们说:

/* dump */
set_what((unsigned int*)&unA);
/* dump */

清单 7

得到:

0x804b06c: 1.3.0000N.A.C.!. 2.00000000000000
0x804b06c: w.h.a.t.N.A.C.!. 2.00000000000000

清单 8

或者:

set_what((unsigned int*)&unB); -> Yield:
0x804b06c: 1.3.0000N.A.C.!. 2.00000000000000
0x804b06c: 1.3.0000N.A.C.!. w.h.a.t.00000000

set_what((unsigned int*)&unC); -> Yield:
0x804b06c: 1.3.0000N.A.C.!. 2.00000000000000
0x804b06c: 1.w.h.a.t.A.C.!. 2.00000000000000

清单 9

正如人们所看到的,数据被覆盖了,无论类型如何。

在某些情况下,这会导致 SIGSEGV。


对于您代码中的问题,如前面的评论所述,但我重复一遍。

在声明中你说int steps,在fscanf()中你指定%li 这是long int 而不是int。在少数几个系统上,这可能有 影响不大,但在 64 位系统上一切都会变糟。

通过 asm 检查:

我们复制代码并制作两份,一份是long int steps;,一份是 int steps; 命名为 Alin_ok.cBlin_bad.c。然后我们创建一些 asm 输出。

A $ cpp lin_ok.c > lin_ok_m32.i
A $ cpp lin_ok.c > lin_ok_m64.i
B $ cpp lin_bad.c > lin_bad_m32.i
B $ cpp lin_bad.c > lin_bad_m64.i

A $ gcc -std=c89 -m32 -S lin_ok_m32.i
A $ gcc -std=c89 -m64 -S lin_ok_m64.i
B $ gcc -std=c89 -m32 -S lin_bad_m32.i
B $ gcc -std=c89 -m64 -S lin_bad_m64.i


$ diff lin_ok_m32.s lin_ok_m64.s | head
9c9
<   .comm   steps,4,4   ; reserve 4 bytes
---
>   .comm   steps,8,8   ; reserve 8 bytes
...

正如我们所看到的,代码指示在 64 位上保留 8 个字节,在 32 位上保留 4 个字节 (本系统)为steps


如果您使用 gcc,请使用更多标志进行编译。我个人通常使用:

gcc -Wall- Wextra -pedantic -std=c89 -o main main.c 或 -std=c99 如有需要。

这会在scanf类型错误等问题上给你警告。


正在运行的应用程序的布局示例。可以完全不同, 取决于系统等,但它是一个近似的 AFAIK。希望我得到了 大部分是对的。

 ________________                       _________________
[                ]                     [                 ]
[                ]                     [ Physical memory ]
[ Virtual memory ] <-- Translation --> [                 ]
[     range      ]        table         - - - - - - - - 
[________________]                     [                 ]
    |                                  [_________________]
    |
 +--+ high address : Virtual address
 |
0xF00 +-------------------+'''''''''''''''''' Runnning env
      | argv, env-vars, ..|                              |
0xBFF +-------------------+                              | ptr
      |       stack       | <- Running storage, where    |
      |...  grows down ...|  fun_a should return, local  | 0xC0000000 on
      |                   |  variables, env, ...         | linux Intel x86
      |  < huge area  >   |  New frame allocated for     |
      |                   |  recursive calls etc.        |
      |...   grows up  ...|                              |     
      |                   | <- Dynamic memory alloc.     |
      |       heap        |  malloc, etc                 |
0x9e49+-------------------+                              | 
      | double sizeX;     | <- Uninitialized data        |
bss   | ...               |           BSS 000000 ...     |
seg.  | int nCellY        |                              |
      | int steps;        |                              |
0x804c+-------------------+''''''''''''''''''''' Stored '| --- edata
data  |                   |                        on    |
seg.  | int rank = 0;     | <- Initialized data   disk   |
0x804b+-------------------+                         :    | --- etext
      | main()            |                         :    |
text  | mov ecx, edx      | <- Instructions         :    | 0x08048000 on
seg.  | ELF, or the like  |   Layout, link, etc     :    | linux Intel x86
0x8040+-------------------+ ''''''''''''''''''''''''''''''
 |
 +--- low address : Virtual address

图 1。

【讨论】:

哇,这是一个全面的解释。非常感谢您的努力和解释。

以上是关于奇怪的全局变量行为,一旦更改变量名,问题就会消失的主要内容,如果未能解决你的问题,请参考以下文章

PHP 常量PHP 变量全解析(超全局变量变量的8种数据类型等)

将全局变量添加到另一个文件中时的奇怪行为

环境变量

从 JS 更改 css 变量的值会导致奇怪的行为

FORTRAN中如何定义全局变量

提升局部变量掩盖全局变量?