Linker multiple definitions error (code used to work but broken after upgrade)

Skip to first unread message

David Good

unread,
Jul 26, 2024, 1:54:59 PM7/26/24
to throwth...@googlegroups.com
I'm really struggling with a situation that I find difficult to explain. I have many years worth of working test code , but after upgrading to Debian 12 from Debian 10 , my tests are all breaking .

The linker error is multiple definition of "xxx register name" . I see the problem , but I can't figure out why it ever worked in the first place .

I have a header file with all of the hardware register definitions called mcu_defs.h and another header file called "compiler_defs.h" which changes the meaning of register definitions depending on whether I'm compiling for PC or target hardware , which is exactly what you want for testing . The way it works is that when compiling for Test , the macros just create regular variables which can be set , changed , inspected , etc . The problem is that if I use mcu_defs.h in more than one source file , the linker (rightly !) thinks that I've defined multiple variables with the same name , and it breaks .

Obviously , the correct "C" way out of this is to have one copy of the vars somewhere (in a mcu_regs.c or something) and have the compiler_defs resolve to externs to those vars , but like I said , I have multiple projects for over 10 years all using this scheme , and they seem to all be broken now .

I'm reeling , to say the least .

Is it possible that gcc changed its behavior somehow ? But again , why / how did it ever work ?

I've attached the smallest project which shows the problem .

Any thoughts ??

Ceedling:: 0.31.1
Unity:: 2.5.4
CMock:: 2.5.4
CException:: 1.3.3

--David
reg_collision2.zip

Mark Vander Voord

unread,
Jul 26, 2024, 2:38:57 PM7/26/24
to throwth...@googlegroups.com
Hey David:

I believe I've run into this same problem in the past. My understanding (and I could be 100% wrong with this) is that at one point in the past gcc would silently merge variables that were obviously supposed to be the same ones. Eventually, I think this was promoted from a warning to an error by default?

In any case, you might want to define TEST for all files and TEST_INSTANCE for just your test files. The TEST_INSTANCE can be used to declare the actual instance once and only once. For example, you might have something like this:

#ifdef TEST
#ifdef TEST_INSTANCE
volatile uint32_t REGISTER_A = 0;
#else
extern volatile uint32_t REGISTER_A;
#endif
#else
#define REGISTER_A (*(volatile uint32_t*)(0x10000000))
#endif

You can see three declarations for each register. The first is the testable instance... the declaration of the variable. It's only enabled during tests and only when compiling the test file itself. The second is a testable reference to that variable which is used by ALL your code during a test. Finally, the last version is the "real" implementation of your register.

I know, it's painful. But it's a pattern that works really well.

Mark

David Good

unread,
Jul 26, 2024, 6:45:40 PM7/26/24
to throwth...@googlegroups.com
Yes , this does work . The modification I'm using is :

#ifdef INSTANTIATE_REGISTER_VARS
#define REGISTER_EXTERN
#else
#define REGISTER_EXTERN extern
#endif

# define SBIT(name, addr, bit)  REGISTER_EXTERN unsigned char name;
# define SFR(name, addr)        REGISTER_EXTERN unsigned char name;
# define SFR16(name, addr)      REGISTER_EXTERN unsigned short name;

This doesn't look too bad and doesn't touch too many files .

Thanks Mark !

--David

--
You received this message because you are subscribed to the Google Groups "ThrowTheSwitch Forums" group.
To unsubscribe from this group and stop receiving emails from it, send an email to throwtheswitc...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/throwtheswitch/CAAu8-XEuDTo53P7YEmSqaQXAyK8z_XUAr_uDCSgArm%2BtRaZk7g%40mail.gmail.com.

David Good

unread,
Jul 26, 2024, 7:55:03 PM7/26/24
to throwth...@googlegroups.com
Ugg ... I spoke too soon . This works for the simple case , but seems to fail when applied to my real codebase for some reason .

--David

Mark Vander Voord

unread,
Jul 26, 2024, 9:09:33 PM7/26/24
to throwth...@googlegroups.com
Are you perhaps running into a situation where it's including both this file and a different file?
There must be something different about the two situations. ;)

David Good

unread,
Jul 29, 2024, 1:26:03 PM7/29/24
to throwth...@googlegroups.com
Looks like it might have something to do with mocking . I put a compiler warning message in the compiler_defs.h to get some idea of when the macros were creating variables and when they were creating external references .

Output :
Test 'test_systime.c'
---------------------
Generating include list for watchdog.h...
Creating mock for watchdog...
In file included from test/systime/test_systime.c:3:
../src/compiler_defs.h:681:2: warning: #warning "instantiating register vars" [-Wcpp]
  681 | #warning "instantiating register vars"
      |  ^~~~~~~
Generating runner for test_systime.c...
Compiling test_systime_runner.c...
In file included from build/test/mocks/mock_watchdog.h:6,
                 from build/test/runners/test_systime_runner.c:6:
../src/../src/compiler_defs.h:684:2: warning: #warning "extern register vars" [-Wcpp]
  684 | #warning "extern register vars"
      |  ^~~~~~~
Compiling test_systime.c...
In file included from test/systime/test_systime.c:3:
../src/compiler_defs.h:681:2: warning: #warning "instantiating register vars" [-Wcpp]
  681 | #warning "instantiating register vars"
      |  ^~~~~~~
Compiling mock_watchdog.c...
In file included from build/test/mocks/mock_watchdog.h:6,
                 from build/test/mocks/mock_watchdog.c:6:
../src/../src/compiler_defs.h:684:2: warning: #warning "extern register vars" [-Wcpp]
  684 | #warning "extern register vars"
      |  ^~~~~~~
Compiling unity.c...
Compiling systime.c...
In file included from ../src/platform_efm8ub2/systime.c:13:
../src/compiler_defs.h:684:2: warning: #warning "extern register vars" [-Wcpp]
  684 | #warning "extern register vars"
      |  ^~~~~~~
Compiling cmock.c...
Linking test_systime.out...
/usr/bin/ld: build/test/out/c/systime.o: warning: relocation against `TMR2CN0_TR2' in read-only section `.text'
/usr/bin/ld: build/test/out/c/systime.o: in function `Timer2_ISR':
/home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:30: undefined reference to `TMR2CN0_TF2H'
/usr/bin/ld: build/test/out/c/systime.o: in function `systime_init':
/home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:36: undefined reference to `IE_ET2'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:42: undefined reference to `CKCON0'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:42: undefined reference to `CKCON0'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:43: undefined reference to `TMR2CN0'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:45: undefined reference to `TMR2RLH'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:46: undefined reference to `TMR2RLL'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:47: undefined reference to `TMR2RLH'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:47: undefined reference to `TMR2H'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:48: undefined reference to `TMR2RLL'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:48: undefined reference to `TMR2L'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:50: undefined reference to `TMR2CN0_TR2'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:53: undefined reference to `IP_PT2'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:58: undefined reference to `IE_ET2'
/usr/bin/ld: build/test/out/c/systime.o: in function `systime_now':
/home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:64: undefined reference to `IE_ET2'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:66: undefined reference to `IE_ET2'
/usr/bin/ld: build/test/out/c/systime.o: in function `systime16_now':
/home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:88: undefined reference to `IE_ET2'
/usr/bin/ld: /home/dgood/samba/pv/dev/PCU/Firmware/PriceVision/test/../src/platform_efm8ub2/systime.c:90: undefined reference to `IE_ET2'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status
ERROR: Shell command failed.
> Shell executed command:
'gcc "build/test/out/c/test_systime_runner.o" "build/test/out/c/test_systime.o" "build/test/out/c/mock_watchdog.o" "build/test/out/c/unity.o" "build/test/out/c/systime.o" "build/test/out/c/cmock.o" -o "build/test/out/test_systime.out"'
> And exited with status: [1].

and here's my Includes section in the test file :

#include "unity.h"
#define INSTANTIATE_REGISTER_VARS
#include "compiler_defs.h"
#include "systime.h"
#include "mock_watchdog.h"

The warnings show that the mocks are instantiating the vars , which is surprising and not what I want , and the test is instantiating the vars as well , but the link error is "undefined reference to 'TMR2CN0_TF2H'" . The warning is also a clue , I think : "warning: relocation against `TMR2CN0_TR2' in read-only section `.text'" .

I've never seen this error anywhere and searching for it leads to discussions about Position Independent Code (PIC and -fPIC flag) which I gather is used for shared libraries .

I'll probably figure something out , but any ideas would be appreciated .

Thanks !

--David

On Fri, Jul 26, 2024 at 8:09 PM Mark Vander Voord <mvande...@gmail.com> wrote:
Are you perhaps running into a situation where it's including both this file and a different file?
There must be something different about the two situations. ;)

--
You received this message because you are subscribed to the Google Groups "ThrowTheSwitch Forums" group.
To unsubscribe from this group and stop receiving emails from it, send an email to throwtheswitc...@googlegroups.com.

David Good

unread,
Jul 29, 2024, 1:43:13 PM7/29/24
to throwth...@googlegroups.com
No , mocking isn't it . I added mocking to my simple project and it works as expected .

--David

David Good

unread,
Jul 30, 2024, 5:02:23 PM7/30/24
to throwth...@googlegroups.com

I finally found the problem . GCC linker changed behavior where this "ambiguous" multiple same global variables was accepted by default and as of 10.0 , it is an error by default . Linker flag -fcommon restores the previous behavior , but is not recommended (flag -fno-common is now the default) .

Explanation : https://stackoverflow.com/questions/69908418/multiple-definition-of-first-defined-here-on-gcc-10-2-1-but-not-gcc-8-3-0

I put the following in my project.yml :

:flags:
  :test:
    :compile:
      :*:
        - -fcommon
    :link:
      :*:
        - -fcommon

This works and allows my projects to run as before .

What happened was that when I first setup my Ceedling / Unity tools , I found something that worked and didn't think too hard about it , but now I see the true error in my setup . I think I can figure out the correct way to restructure these defines , but at least now the pressure is off and I can get back to the tasks which have been piling up ever since this upgrade started lol .

Thanks !

--David


--David

David Good

unread,
Jul 30, 2024, 5:40:10 PM7/30/24
to throwth...@googlegroups.com
It's actually GCC compiler and linker , as reflected by my flags fix from above .

--David
Reply all
Reply to author
Forward
0 new messages