DEV Community

codemee
codemee

Posted on

ESP32 有些變數名稱不能用--兼談 C++ 的連結性 (linkage)

今天使用 ESP32 時遇到一個莫名其妙的雷, 先來看看以下這個程式:

int times = 32;

void setup() {
  Serial.begin(115200);
  Serial.print("\ntimes = ");
  Serial.println(times);
}

void loop() {}
Enter fullscreen mode Exit fullscreen mode

你覺得執行後會輸出什麼呢?

其實會輸出什麼要看你使用哪一個版本的 ESP32 Arduino Core, 如果是使用 2.0.0 之前的版本, 輸出結果就是如同大家想的一樣:

times = 32
Enter fullscreen mode Exit fullscreen mode

可是如果你是使用 2.0.0 之後的版本 (至少到我今天使用的 2.0.2), 你會看到如下的輸出:

times = 620773686
Enter fullscreen mode Exit fullscreen mode

這是什麼奇怪的妖術?直覺地想顯然我列印的變數 times 不是我程式裡宣告的 times, 但為什麼呢?

尋找 times 變數

還好有開放原始碼, 經過在 ESP32 Arduino Core 的 github 上搜尋, 發現有一個連結器 (linker) 使用的 esp32.rom.syscalls.ld 檔, 裡面有這樣的一段:

close = 0x40001778;
open = 0x4000178c;
read = 0x400017dc;
sbrk = 0x400017f4;
times = 0x40001808;
write = 0x4000181c;
Enter fullscreen mode Exit fullscreen mode

所以應該是連結時把 times 重新定位到 0x40001808 這個位址了, 所以才會讀到奇怪的數值。為了驗證這一點, 只要印出 times 的位址來比對就知道了:

int times = 32;

char buf[20];

void setup() {
  Serial.begin(115200);
  Serial.print("\n&times = ");
  sprintf(buf, "%p", &times);
  Serial.println(buf);
}

void loop() {}
Enter fullscreen mode Exit fullscreen mode

輸出結果如下:

&times = 0x40001808
Enter fullscreen mode Exit fullscreen mode

果然沒錯, 我在程式裡宣告的 times 變數被連結器重新定位了。但是為什麼會發生這樣的事呢?

C++ 的連結性 (linkage)

C++ 對於每一個符號都有訂定它的連結性 (linkage), 在檔案中定義的全域變數如果沒有指定, 就會具備外部連結性 (external linkage), 意思就是其他檔案內的程式也可以使用這個變數。因此, 像是我一開始舉的例子:

int times = 32;
Enter fullscreen mode Exit fullscreen mode

這個 times 就具備外部連結性, 它跟以下使用 extern 明確指定外部連結性是一樣的:

extern int times = 32;
Enter fullscreen mode Exit fullscreen mode

具有外部連結性的變數因為要能夠給其他檔案中的程式使用, 所以必須出現在符號表中, 才能依據其位址將其他檔案中用到此變數的地方重新定位到正確的位址。但是我們剛剛看到的 esp32.rom.syscalls.ld 檔的內容, 卻定義了同名的符號, 強制將 times 定位到指定的位址, 造成讀取變數內容時跑到不對的地方取值, 當然就讀到錯誤的值了。

使用 static 指定內部連結性 (internal linkage) 避免問題

要避免本文提到的問題, 最簡單的方法就是使用 static 明確指定內部連結性(internal linkage), 這樣就不會讓變數可以讓其他程式檔使用, 也就不會出現在符號表中, 自然就不會被 esp32.rom.syscalls.ld 檔的作用影響。修改後的程式如下:

static int times = 32;

void setup() {
  Serial.begin(115200);
  Serial.print("\ntimes = ");
  Serial.println(times);
}

void loop() {}
Enter fullscreen mode Exit fullscreen mode

輸出的結果就是正確的 32 了:

times = 32
Enter fullscreen mode Exit fullscreen mode

小結

對於在程式中宣告的全域變數, 如果沒有要給外部使用, 那麼最好養成使用 static 指定為內部連結性的好習慣, 不然難保哪一天會像我一樣採到雷, 花個一下午就為了找問題, 實在得不償失。

Discussion (6)

Collapse
andypiper profile image
Andy Piper

Wow, this is interesting, thank you for the explanation! I need to remember static for my own code in the future.

Collapse
codemee profile image
codemee Author

You can read Chinese words, cool.

Collapse
andypiper profile image
Andy Piper

Well I used a translation tool, I am interested to learn how people use the ESP32. Thank you.

Thread Thread
codemee profile image
codemee Author

Wow, thank you.

Collapse
opabravo profile image
FateWalker • Edited on

作者的每篇文章,品質都很高,推!
都是非常實用的經驗與觀念
發現作者很愛用"妖術"這個詞,哈哈哈

Collapse
codemee profile image
codemee Author

因為我很想要擁有妖術!