内联汇编中的结构偏移
Struct offsets in inline assembly
我正在做一个项目,其中一些中断服务必须在汇编程序中处理。从中断向量包装器调用处理函数。处理程序 body 是用汇编程序编写的,它在特定寄存器中接收单个(指针)参数。
代码目标是MSP430,它必须同时使用MSP430-gcc 和TI 编译器进行编译。我已经有了 MSP430-gcc 的工作解决方案,它看起来像这样:
static void __attribute__((naked)) _shared_vector_handler(Timer_driver_t *driver) {
__asm__(
" MOVX.W %c[iv_register_offset](R12),R14 ; \n"
" ADD @R14,PC ; \n"
" RETA ; \n"
" JMP CCIFG_1_HND ; Vector 2 \n"
" JMP CCIFG_2_HND ; Vector 4 \n"
" JMP CCIFG_3_HND ; Vector 6 \n"
" JMP CCIFG_4_HND ; Vector 8 \n"
" JMP CCIFG_5_HND ; Vector 10 \n"
" JMP CCIFG_6_HND ; Vector 12 \n"
"TIFG_HND: \n"
" MOVX.A %c[overflow_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_1_HND: \n"
" MOVX.A %c[ccr1_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_2_HND: \n"
" MOVX.A %c[ccr2_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_3_HND: \n"
" MOVX.A %c[ccr3_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_4_HND: \n"
" MOVX.A %c[ccr4_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_5_HND: \n"
" MOVX.A %c[ccr5_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_6_HND: \n"
" MOVX.A %c[ccr6_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n" ::
[iv_register_offset] "i" (offsetof(Timer_driver_t, _IV_register)),
[overflow_handle_offset] "i" (offsetof(Timer_driver_t, _overflow_handle)),
[ccr1_handle_offset] "i" (offsetof(Timer_driver_t, _CCR1_handle)),
[ccr2_handle_offset] "i" (offsetof(Timer_driver_t, _CCR2_handle)),
[ccr3_handle_offset] "i" (offsetof(Timer_driver_t, _CCR3_handle)),
[ccr4_handle_offset] "i" (offsetof(Timer_driver_t, _CCR4_handle)),
[ccr5_handle_offset] "i" (offsetof(Timer_driver_t, _CCR5_handle)),
[ccr6_handle_offset] "i" (offsetof(Timer_driver_t, _CCR6_handle)),
[handler_offset] "i" (offsetof(Timer_channel_handle_t, _handler)),
[handler_param_offset] "i" (offsetof(Timer_channel_handle_t, _handler_param)) :
);
}
翻译成英文:driver 结构包含某个特定偏移量上的 IV 寄存器地址。该地址上的内容被添加到 PC,因此跳转到特定标签(取决于设置的中断标志)。这是 TI 在 user's guide,第 653 页中描述的推荐用法。所有标签都做同样的事情:它们从特定偏移量的 driver 结构中获取指向某个句柄的指针。该句柄再次具有一些特定的偏移函数指针(中断服务处理程序)和指向某个参数的指针,这些参数将传递给处理程序。结构简述:
typedef struct Timer_driver {
// enable dispose(Timer_driver_t *)
Disposable_t _disposable;
// base of HW timer registers, (address of corresponding TxCTL register)
uint16_t _CTL_register;
...
// interrupt vector register
uint16_t _IV_register;
// stored mode control
uint8_t _mode;
// amount of CCRn registers
uint8_t _available_handles_cnt;
// main (CCR0) handle
Timer_channel_handle_t *_CCR0_handle;
// up to six (CCRn) handles sharing one interrupt vector
Timer_channel_handle_t *_CCR1_handle;
Timer_channel_handle_t *_CCR2_handle;
...
}
和
struct Timer_channel_handle {
// vector wrapper, enable dispose(Timer_channel_handle_t *)
Vector_handle_t vector;
// HW timer driver reference
Timer_driver_t *_driver;
// capture / compare control register
uint16_t _CCTLn_register;
// capture / compare register
uint16_t _CCRn_register;
// vector interrupt service handler
void (*_handler)(void *);
// vector interrupt service handler parameter
void *_handler_param;
...
}
现在是问题。
- 直到编译时才知道偏移量
- 我无法将一些 offsetof(s, m) 传递给汇编器
- 偏移量取决于使用的内存模型(指针大小 16 位或 32 位)
- 偏移量取决于两个结构的第一个成员的大小,此大小取决于预处理器定义(1 个指针或 4 个指针)
- 无法预先计算偏移量,因为每个编译器都会向第一个成员结构添加一些对齐和填充
- 第一个成员必须是第一个成员(不允许重新排序)
- TI 编译器不支持将 compile-time 变量传递给内联汇编代码
目标:
- 支持两种编译器
- 不要复制代码,不要硬编码偏移量
- 如果可能,避免将整个处理程序提取到 asm 文件并通过 .cdecls(或 gcc 的 #include)包含 headers。两个编译器处理包含 C headers 的方式有很大不同,结构偏移的定义方式也有很大不同,并且需要对 headers 进行一些 non-trivial 重组,我相信这是几乎不可能。
当我用 TI 编译器编译时出现以下错误:
"../module/driver/src/timer.c", line 274: error #18: expected a ")"
"../module/driver/src/timer.c", line 285: warning #12-D: parsing restarts here after previous syntax error
1 error detected in the compilation of "../module/driver/src/timer.c".
gmake: *** [module/driver/src/timer.obj] Error 1
我的构建是由 CMake 处理的,我可以想到一个解决方案 - 只需将这些偏移量预生成到某个 header 文件中,该文件应包含在 driver 中。 here 描述了如何做到这一点。但如果可能的话,我也想避免这一步,因为它需要在 Code Composer Studio 中编译,而不是 运行 cmake。
那么我该如何创建 CMake 目标来预生成这些偏移量呢?或者有什么其他想法?
对于在 C 预处理器运行时以数字形式可用的常量,您可以使用 #define
宏将它们字符串化并与 inline-asm 字符串连接,例如 asm("blah blah " stringify(MYCONST) "\nblah blah");
,但它赢了不适用于 offsetof
,这需要编译器正确地将其计算为一个数字。
FIXME:这在交叉编译时不会那么容易工作。您必须解析编译器生成的 asm,或从 .o
中转储静态数据,这两种方法都可以作为对该方法的微小修改,但有点难看。我将在这里留下这个答案,以防它对非交叉编译用例有用。
但是,由于您标记了这个 cmake
,您拥有了一个可以处理依赖链的构建系统。 您可以编写一个使用 offsetof
的程序,使用一些简单的 printf
语句 创建 一个具有类似内容的 .h
.h
=39=]
// already stringified to simplify
// if you want them as numeric literals, leave out the double quotes and use a STR() macro
#define OFFSET_Timer_driver_t__CCR1_handle "12"
#define OFFSET_Timer_driver_t__CCR2_handle "16"
...
然后你可以 #include "struct_offsets.h"
在需要它的文件中,并将它用于像
这样的内联汇编
asm("insn " OFFSET_Timer_driver_t__CCR1_handle "\n\t"
"insn blah, blah \n\t"
"insn foo " OFFSET_Timer_driver_t__CCR2_handle "\n\t"
);
或者使用纯 asm 而不是裸函数,因为你是。
使用 CMake 构建依赖项,以便任何需要 struct_offsets.h
的文件在更改时重建。
感谢大家,特别感谢@CL。由于很多原因,我一直认为这必须在汇编程序中完成,而且我只需要以某种方式获得这些偏移量。解决方法很简单:
static void _shared_vector_handler(Timer_driver_t *driver) {
uint16_t interrupt_source;
Timer_channel_handle_t *handle;
if ( ! (interrupt_source = hw_register_16(driver->_IV_register))) {
return;
}
handle = *((Timer_channel_handle_t **)
(((uintptr_t)(&driver->_CCR0_handle)) + (interrupt_source * _POINTER_SIZE_ / 2)));
(*handle->_handler)(handle->_handler_param);
}
翻译成汇编程序(TI 编译器,大内存模型):
_shared_vector_handler():
011ef6: 4C1F 0008 MOV.W 0x0008(R12),R15
011efa: 4F2F MOV.W @R15,R15
011efc: 930F TST.W R15
011efe: 240D JEQ (0x1f1a)
231 (*handle->_handler)(handle->_handler_param);
011f00: F03F 3FFF AND.W #0x3fff,R15
011f04: 025F RLAM.W #1,R15
011f06: 4F0F MOV.W R15,R15
011f08: 00AC 000C ADDA #0x0000c,R12
011f0c: 0FEC ADDA R15,R12
011f0e: 0C0F MOVA @R12,R15
011f10: 0F3C 003E MOVA 0x003e(R15),R12
011f14: 00AF 003A ADDA #0x0003a,R15
011f18: 0F00 BRA @R15
$C$L12:
011f1a: 0110 RETA
原来的汇编程序需要 7 条指令来执行处理程序,但 add-IV-to-PC 会破坏流水线。这里我们有 13 条指令,因此效率几乎相等。
顺便说一句,实际提交是 here。
我正在做一个项目,其中一些中断服务必须在汇编程序中处理。从中断向量包装器调用处理函数。处理程序 body 是用汇编程序编写的,它在特定寄存器中接收单个(指针)参数。
代码目标是MSP430,它必须同时使用MSP430-gcc 和TI 编译器进行编译。我已经有了 MSP430-gcc 的工作解决方案,它看起来像这样:
static void __attribute__((naked)) _shared_vector_handler(Timer_driver_t *driver) {
__asm__(
" MOVX.W %c[iv_register_offset](R12),R14 ; \n"
" ADD @R14,PC ; \n"
" RETA ; \n"
" JMP CCIFG_1_HND ; Vector 2 \n"
" JMP CCIFG_2_HND ; Vector 4 \n"
" JMP CCIFG_3_HND ; Vector 6 \n"
" JMP CCIFG_4_HND ; Vector 8 \n"
" JMP CCIFG_5_HND ; Vector 10 \n"
" JMP CCIFG_6_HND ; Vector 12 \n"
"TIFG_HND: \n"
" MOVX.A %c[overflow_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_1_HND: \n"
" MOVX.A %c[ccr1_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_2_HND: \n"
" MOVX.A %c[ccr2_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_3_HND: \n"
" MOVX.A %c[ccr3_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_4_HND: \n"
" MOVX.A %c[ccr4_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_5_HND: \n"
" MOVX.A %c[ccr5_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n"
" RETA ; \n"
"CCIFG_6_HND: \n"
" MOVX.A %c[ccr6_handle_offset](R12),R14 ; \n"
" MOVX.A %c[handler_param_offset](R14),R12 ; \n"
" MOVX.A %c[handler_offset](R14),R14 ; \n"
" CALLA R14 ; \n" ::
[iv_register_offset] "i" (offsetof(Timer_driver_t, _IV_register)),
[overflow_handle_offset] "i" (offsetof(Timer_driver_t, _overflow_handle)),
[ccr1_handle_offset] "i" (offsetof(Timer_driver_t, _CCR1_handle)),
[ccr2_handle_offset] "i" (offsetof(Timer_driver_t, _CCR2_handle)),
[ccr3_handle_offset] "i" (offsetof(Timer_driver_t, _CCR3_handle)),
[ccr4_handle_offset] "i" (offsetof(Timer_driver_t, _CCR4_handle)),
[ccr5_handle_offset] "i" (offsetof(Timer_driver_t, _CCR5_handle)),
[ccr6_handle_offset] "i" (offsetof(Timer_driver_t, _CCR6_handle)),
[handler_offset] "i" (offsetof(Timer_channel_handle_t, _handler)),
[handler_param_offset] "i" (offsetof(Timer_channel_handle_t, _handler_param)) :
);
}
翻译成英文:driver 结构包含某个特定偏移量上的 IV 寄存器地址。该地址上的内容被添加到 PC,因此跳转到特定标签(取决于设置的中断标志)。这是 TI 在 user's guide,第 653 页中描述的推荐用法。所有标签都做同样的事情:它们从特定偏移量的 driver 结构中获取指向某个句柄的指针。该句柄再次具有一些特定的偏移函数指针(中断服务处理程序)和指向某个参数的指针,这些参数将传递给处理程序。结构简述:
typedef struct Timer_driver {
// enable dispose(Timer_driver_t *)
Disposable_t _disposable;
// base of HW timer registers, (address of corresponding TxCTL register)
uint16_t _CTL_register;
...
// interrupt vector register
uint16_t _IV_register;
// stored mode control
uint8_t _mode;
// amount of CCRn registers
uint8_t _available_handles_cnt;
// main (CCR0) handle
Timer_channel_handle_t *_CCR0_handle;
// up to six (CCRn) handles sharing one interrupt vector
Timer_channel_handle_t *_CCR1_handle;
Timer_channel_handle_t *_CCR2_handle;
...
}
和
struct Timer_channel_handle {
// vector wrapper, enable dispose(Timer_channel_handle_t *)
Vector_handle_t vector;
// HW timer driver reference
Timer_driver_t *_driver;
// capture / compare control register
uint16_t _CCTLn_register;
// capture / compare register
uint16_t _CCRn_register;
// vector interrupt service handler
void (*_handler)(void *);
// vector interrupt service handler parameter
void *_handler_param;
...
}
现在是问题。
- 直到编译时才知道偏移量
- 我无法将一些 offsetof(s, m) 传递给汇编器
- 偏移量取决于使用的内存模型(指针大小 16 位或 32 位)
- 偏移量取决于两个结构的第一个成员的大小,此大小取决于预处理器定义(1 个指针或 4 个指针)
- 无法预先计算偏移量,因为每个编译器都会向第一个成员结构添加一些对齐和填充
- 第一个成员必须是第一个成员(不允许重新排序)
- TI 编译器不支持将 compile-time 变量传递给内联汇编代码
目标:
- 支持两种编译器
- 不要复制代码,不要硬编码偏移量
- 如果可能,避免将整个处理程序提取到 asm 文件并通过 .cdecls(或 gcc 的 #include)包含 headers。两个编译器处理包含 C headers 的方式有很大不同,结构偏移的定义方式也有很大不同,并且需要对 headers 进行一些 non-trivial 重组,我相信这是几乎不可能。
当我用 TI 编译器编译时出现以下错误:
"../module/driver/src/timer.c", line 274: error #18: expected a ")"
"../module/driver/src/timer.c", line 285: warning #12-D: parsing restarts here after previous syntax error
1 error detected in the compilation of "../module/driver/src/timer.c".
gmake: *** [module/driver/src/timer.obj] Error 1
我的构建是由 CMake 处理的,我可以想到一个解决方案 - 只需将这些偏移量预生成到某个 header 文件中,该文件应包含在 driver 中。 here 描述了如何做到这一点。但如果可能的话,我也想避免这一步,因为它需要在 Code Composer Studio 中编译,而不是 运行 cmake。
那么我该如何创建 CMake 目标来预生成这些偏移量呢?或者有什么其他想法?
对于在 C 预处理器运行时以数字形式可用的常量,您可以使用 #define
宏将它们字符串化并与 inline-asm 字符串连接,例如 asm("blah blah " stringify(MYCONST) "\nblah blah");
,但它赢了不适用于 offsetof
,这需要编译器正确地将其计算为一个数字。
FIXME:这在交叉编译时不会那么容易工作。您必须解析编译器生成的 asm,或从 .o
中转储静态数据,这两种方法都可以作为对该方法的微小修改,但有点难看。我将在这里留下这个答案,以防它对非交叉编译用例有用。
但是,由于您标记了这个 cmake
,您拥有了一个可以处理依赖链的构建系统。 您可以编写一个使用 offsetof
的程序,使用一些简单的 printf
语句 创建 一个具有类似内容的 .h
.h
=39=]
// already stringified to simplify
// if you want them as numeric literals, leave out the double quotes and use a STR() macro
#define OFFSET_Timer_driver_t__CCR1_handle "12"
#define OFFSET_Timer_driver_t__CCR2_handle "16"
...
然后你可以 #include "struct_offsets.h"
在需要它的文件中,并将它用于像
asm("insn " OFFSET_Timer_driver_t__CCR1_handle "\n\t"
"insn blah, blah \n\t"
"insn foo " OFFSET_Timer_driver_t__CCR2_handle "\n\t"
);
或者使用纯 asm 而不是裸函数,因为你是。
使用 CMake 构建依赖项,以便任何需要 struct_offsets.h
的文件在更改时重建。
感谢大家,特别感谢@CL。由于很多原因,我一直认为这必须在汇编程序中完成,而且我只需要以某种方式获得这些偏移量。解决方法很简单:
static void _shared_vector_handler(Timer_driver_t *driver) {
uint16_t interrupt_source;
Timer_channel_handle_t *handle;
if ( ! (interrupt_source = hw_register_16(driver->_IV_register))) {
return;
}
handle = *((Timer_channel_handle_t **)
(((uintptr_t)(&driver->_CCR0_handle)) + (interrupt_source * _POINTER_SIZE_ / 2)));
(*handle->_handler)(handle->_handler_param);
}
翻译成汇编程序(TI 编译器,大内存模型):
_shared_vector_handler():
011ef6: 4C1F 0008 MOV.W 0x0008(R12),R15
011efa: 4F2F MOV.W @R15,R15
011efc: 930F TST.W R15
011efe: 240D JEQ (0x1f1a)
231 (*handle->_handler)(handle->_handler_param);
011f00: F03F 3FFF AND.W #0x3fff,R15
011f04: 025F RLAM.W #1,R15
011f06: 4F0F MOV.W R15,R15
011f08: 00AC 000C ADDA #0x0000c,R12
011f0c: 0FEC ADDA R15,R12
011f0e: 0C0F MOVA @R12,R15
011f10: 0F3C 003E MOVA 0x003e(R15),R12
011f14: 00AF 003A ADDA #0x0003a,R15
011f18: 0F00 BRA @R15
$C$L12:
011f1a: 0110 RETA
原来的汇编程序需要 7 条指令来执行处理程序,但 add-IV-to-PC 会破坏流水线。这里我们有 13 条指令,因此效率几乎相等。
顺便说一句,实际提交是 here。