Shouldn't the "used" tag alone be enough?
It is not sufficient, and it is not necessary. It is not relevant.
As per the GCC documentation that you have quoted, attribute used is
applicable to definitions of static variables. And as an answer that is now
deleted by the author pointed out, your ApplicationID is not static, so attribute used
has no effect.
Here:
/* app_id_extern.c */
#include <stdint.h>
const uint8_t   ApplicationID[16] = {
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x12, 0x34, 0x00, 0x00
};
we have ApplicationID defined, by default, as an extern variable. The default storage class
of a filescope variable, like ApplicationID, is extern. The compiler will
accordingly generate an object file in which the definition of ApplicationID
is exposed for linkage, as we can see:
$ gcc -c app_id_extern.c
$ readelf -s app_id_extern.o
Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     ...
     9: 0000000000000000    16 OBJECT  GLOBAL DEFAULT    4 ApplicationID
In the object file, ApplicationID is a 16-byte GLOBAL symbol in section #4
(which in this case happens to be .rodata). The GLOBAL binding means that the static linker can see this symbol.
And here:
/* app_id_static.c */
#include <stdint.h>
static const uint8_t   ApplicationID[16] = {
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x12, 0x34, 0x00, 0x00
};
we have ApplicationID explicitly defined as a static variable. The compiler
will accordingly generate an object file in which the definition of ApplicationID
is not exposed for linkage, as we can also see:
$ gcc -c app_id_static.c
$ readelf -s app_id_static.o
Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     ...
     6: 0000000000000000    16 OBJECT  LOCAL  DEFAULT    4 ApplicationID
     ...
In this object file, ApplicationID is a 16-byte LOCAL symbol in the .rodata section
The LOCAL binding means that the static linker cannot see this symbol.
The compiler will always emit in the object file a definition of an extern variable,
like that of ApplicationID in app_id_extern.c, even if that definition is not
referenced in the object file, because the external definition will be available to
the linker, and therefore might by be referenced at linktime from other object files, for all that
the compiler can possibly know.
But if a variable is static, then the compiler knows that its definition is unavailable
for linkage. So if it can determine that the definition is not referenced within the
object file itself, it may conclude that the definition is redundant and not emit it
in the object file at all. Like so:
$ gcc -O1 -c app_id_static.c
This time, we ask the compiler to perform minimal optimizations. And then
$ readelf -s app_id_static.o
Symbol table '.symtab' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS app_id_static.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
the unreferenced definition of ApplicationID is no longer present in the object file
at all. It was optimized out.
Now for certain unusual applications we may want the compiler to emit the definition
of a symbol in an object file that does not refer to it, and conceal it from the static linker. That is
where attribute used comes into play:
/* app_id_static_used .c */
#include <stdint.h>
static const uint8_t   ApplicationID[16] __attribute__((used,section(".rodata.$AppID"))) = {
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x12, 0x34, 0x00, 0x00
};
Once again we compile with -O1 optimization:
$ gcc -O1 -c app_id_static_used.c
$ readelf -s app_id_static_used.o
Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     ...
     6: 0000000000000000    16 OBJECT  LOCAL  DEFAULT    4 ApplicationID
     ...
But this time, thanks to attribute used, the LOCAL definition of ApplicationID
reappears in section #4 (which in this object file is .rodata.$AppID)
That is how attribute used works. It affects the behaviour of the compiler: it has no
influence on the linker.
We haven't done any linkage yet. Let's do some now.
/* hello_world.c */
#include <stdio.h>
int main(void)
{
    puts("Hello world!")
    return 0;
}
This program makes no reference to ApplicationID, but we'll input app_id_static_used.o
to the linkage regardless:
$ gcc -O1 -c hello_world.c
$ gcc -o hello hello_world.o app_id_static_used.o -Wl,-gc-sections,-Map=mapfile.txt
In the linkage, I have asked for unused input sections to be dropped, and for a mapfile
to be output (-Wl,-gc-sections,-Map=mapfile.txt)
In the mapfile we find:
Mapfile.txt
...
Discarded input sections
  ...
  .rodata.$AppID
                0x0000000000000000       0x10 app_id_static_used.o
  ...
The linker has discarded section .rodata.$AppID input from app_id_static_used.o
because no symbol defined in that section is referenced in the program. With
attribute used, we compelled the compiler to emit the definition of that static symbol
in app_id_static_used.o. That doesn't compell the linker to need it, or keep
it in the executable.
In we switch from app_id_static_used.c to:
/* app_id_extern_used.c */
#include <stdint.h>
const uint8_t   ApplicationID[16] __attribute__((used,section(".rodata.$AppID"))) = {
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x12, 0x34, 0x00, 0x00
};
then we're doing what you did, applying attribute used to an extern definition. Attribute
used has no effect in that case, because the compiler is bound to emit the extern
definition in any case. And the linker will still discard the .rodata.$AppID input section from the
executable if the program does not refer to anything in it.
So far, your app-id source file might as well be:
/* app_id_extern_section.c */
#include <stdint.h>
const uint8_t   ApplicationID[16] __attribute__((section(".rodata.$AppID"))) = {
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x12, 0x34, 0x00, 0x00
};
And what you then need to do is inform the linker that you want the definition of
the symbol ApplicationID kept, even if it is not referenced by your program, and
if even if unused sections are dropped.
To achieve that, use the linker option --undefined=ApplicationID. This will
direct the linker to assume from the start that the linkage of your program
has encountered an undefined reference to ApplicationID and compel the linker
to find and link its definition, if any input file provides one. Thus:
$ gcc -O1 -c app_id_extern_section.c
$ gcc -o hello hello_world.o app_id_extern_section.o -Wl,-gc-sections,--undefined=ApplicationID
Now the program contains the definition of ApplicationID, despite not referring to it:
$ readelf -s hello | grep ApplicationID
    58: 0000000000002010    16 OBJECT  GLOBAL DEFAULT   18 ApplicationID
Section #18 is the .rodata section of the program:
$ readelf --sections hello | grep '.rodata'
  [18] .rodata           PROGBITS         0000000000002000  00002000
Lastly, note that the input section .rodata.$AppID from app_id_extern_section.o
has been merged into the output section .rodata, because the linker's default
linker script specifies:
.rodata         : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
i.e. all input sections matching .rodata, .rodata.* or .gnu.linkonce.r.*
will be output to .rodata. This means that even:
__attribute__((section(".rodata.$AppID")))
is redundant. So the app-id source file might as well simply be the one
I started with, app_id_extern.c, and the linkage option --undefined=ApplicationID
is all that is necessary to keep the unreferenced symbol in the program. Unless
your linker is different in that respect you will find the same.