To get the addresses of individual members, like you rightly pointed out, you have to add a constant offset to the address of the struct. The problem essentially boils down to automatically calculating these offsets for a given struct and member.
Even for small structs, I would suggest not calculating the offsets by hand because compilers add different padding between members and hints like __attribute__(packed) are not available for all compilers.
For most of my projects I use a separate program to generate an offsets header at build time.
The basic idea is to create a .c/.cpp file with something like -
// Your program headers that define the structs here
#include <stdio.h>
#include <stddef.h>
#define OFFSET(x, str, mem) printf("#define " #x " %d\n",(int) offsetof(str, mem))
#define SIZE(x, str) printf("#define " #x " %d\n", (int) sizeof(str))
#define HEADER_START() printf("#ifndef ASM_OFFSET_H\n#define ASM_OFFSET_H\n")
#define HEADER_END() printf("#endif\n")
int main(int argc, char *argv[]) {
HEADER_START();
OFFSET(struct_name_member_name, struct_name, member_name); // Substitute your struct name and member name here
SIZE(struct_name_size, struct_name); // This macro is to get the size of a struct
HEADER_END();
return 0;
}
Executing this program during the build time generates a header file that can be included directly with GASM. For other assemblers you can change the syntax to generate appropriate inc files.
The benefit of this approach is that the program uses the same headers as your C/C++ code. That way when you change any headers, the generated offsets should change automatically.
One thing you have to be careful here is that this technique does not work well if you are cross compiling for a different architecture and the compiler uses a different layout on your host vs the target. If you want something similar for cross compilation, I can write a separate answer.