保证函数调用时stack的起始地址是十六字节对齐的方法

探讨SSE指令集对内存地址的要求及不同编译器间的兼容性问题,提供解决方案。

    在某些应用到SSE指令的代码中,都会要求所参与运算的内存地址必须是16字节对齐,否则程序将会crash. 

在默认情况下如果一个(静态/动态)库是用gcc编译的,并且调用该(静态/动态)库的程序也是用gcc编译的,则该程序将运行正常,因为这样可以保证所有代码中都用相同的stack初始化逻辑,如果代码出错,则必定是真实的错误,而不是由于函数调用过程中stack初始化错误而造成的。

 

    而如果一个库(静态/动态)库用gcc编译,而调用该(静态/动态)库的程序则是MSVC++编译的,则问题就来了,因为gcc可以保证并假定在任何函数调用中所用到的stack都是16字节对齐的,如果在该(静态/动态)库中恰恰用到了stack上的开始地址处变量进行的SSE运算,这理论上是没有问题的,因为stack地址已经自动做了16字节对齐,而MSVC++的函数调用则不满足16字节对齐的要求,一般仅仅做到了4字节对齐,这就使得在后续的SSE运算中程序crash.

 

例如在FFmpeg中有如下代码

 

void ff_fdct_sse2(int16_t *block)

{

    DECLARE_ALIGNED(16, int64_t, align_tmp[16]);

    int16_t * const block1= (int16_t*)align_tmp;

 

    fdct_col_sse2(block, block1, 0);

    fdct_row_sse2(block1, block);

}

 

用gcc编译后会产生如下代码:

 

00000e50 <_ff_fdct_sse2>:

     e50: 81 ec 8c 00 00 00     sub    $0x8c,%esp

     e56: b9 00 00 00 00       mov    $0x0,%ecx

     e5b: 89 9c 24 84 00 00 00 mov    %ebx,0x84(%esp)

     e62: 8b 94 24 90 00 00 00 mov    0x90(%esp),%edx

     e69: bb 30 00 00 00       mov    $0x30,%ebx

     e6e: 89 b4 24 88 00 00 00 mov    %esi,0x88(%esp)

     e75: be 40 00 00 00       mov    $0x40,%esi

     e7a: 66 0f 6f 42 10       movdqa 0x10(%edx),%xmm0

     e7f: 66 0f 6f 4a 60       movdqa 0x60(%edx),%xmm1

     e84: 66 0f 6f d0           movdqa %xmm0,%xmm2

     e88: 66 0f 6f 5a 20       movdqa 0x20(%edx),%xmm3

     e8d: 66 0f ed c1           paddsw %xmm1,%xmm0

     e91: 66 0f 6f 62 50       movdqa 0x50(%edx),%xmm4

     e96: 66 0f 71 f0 03       psllw  $0x3,%xmm0

     e9b: 66 0f 6f 2a           movdqa (%edx),%xmm5

     e9f: 66 0f ed e3           paddsw %xmm3,%xmm4

     ea3: 66 0f ed 6a 70       paddsw 0x70(%edx),%xmm5

     ea8: 66 0f 71 f4 03       psllw  $0x3,%xmm4

     ead: 66 0f 6f f0           movdqa %xmm0,%xmm6

     eb1: 66 0f e9 d1           psubsw %xmm1,%xmm2

......

......

 

通过如下指令直接在栈上开辟出了所有的局部变量所需要的空间:

sub $0x8c, %esp

 

仔细推敲可以发现这样做能保证如下两个局部变量的内存分配可以正常工作的原因是gcc已经假定函数的第一个参数的对应的内存地址是可以被16整除的!

 

 DECLARE_ALIGNED(16, int64_t, align_tmp[16]);

    int16_t * const block1= (int16_t*)align_tmp;

 

而在MSVC++中这个条件是不满足的,如果在MSVC++中使用这个函数,则程序的crash的可能性差不多有75%

 

-----------------------------------------------------------------------

    可以用如下方法之一来解决该问题:

    1)最简单的方式是在导出函数的实现代码中用gcc的__attribute__((force_align_arg_pointer))修饰符,例如

 

int __attribute__((force_align_arg_pointer)) avcodec_open(AVCodecContext *avctx, AVCodec *codec)

{

    //....

}

    关于force_align_arg_pointer的使用的优点和缺点参见如下说明:

 

    On the Intel x86, the force_align_arg_pointer attribute may be applied to individual function definitions, generating an alternate prologue and epilogue that realigns the runtime stack. This supports mixing legacy codes that run with a 4-byte aligned stack with modern codes that keep a 16-byte stack for SSE compatibility. The alternate prologue/epilogue is slower and bigger than the regular one, and it requires one dedicated register for the life of the function. This also lowers the number of registers available if used in conjunction with the regparm attribute. The force_align_arg_pointer attribute is incompatible with nested functions; this is considered a hard error. 

 

    注意要使用force_align_arg_pointer必须保证gcc的版本不小于4.2

 

    2)自己手动维护函数调用的stack对齐实现,此种实现就有多种版本了,要看每个人的实现方法了,下面给出两种示例实现。

 

    a)MSVC风格的调用demo,代码来自x264项目,可以用yasm来编译

 

;-----------------------------------------------------------------------------

; void x264_stack_align( void (*func)(void*), void *arg );

;-----------------------------------------------------------------------------

cglobal x264_stack_align

    push ebp

    mov  ebp, esp

    sub  esp, 4

    and  esp, ~15

    mov  ec x, [ebp+8]

    mov  edx, [ebp+12]

    mov  [esp], edx

    call ecx

    mov  esp, ebp

    pop  ebp

    ret

 

简单说明一下上面代码的实现逻辑:

 

本质上来讲,上面的汇编代码就是模拟了x86下的一次最简单的函数调用过程。

step 1>

 

push ebp;首先将上一次的栈底指针(ebp)保存下来,

mov ebp, esp;将栈顶指针esp赋值给ebp,即ebp = esp,此时ebp和esp都指向刚才保存的上次的ebp的内存的地址

 

此时的栈结构为(从高地址到低地址)

 

              ______________

             |arg

             |_____________

             |func

             |_____________

             |函数返回地址

             |____________

             |old ebp

  ebp-->|_____________

 

step 2>

 

sub esp,4 ;在栈上开辟4字节的预留空间,确保在step 4 中不会将我们保存的上一次的ebp冲掉

and esp,~15 ;继续增加stack的size,强制栈顶指针 esp 16字节对齐,注意在x86上stack的增长方向是从高地址到的地址

 

              ______________

             |arg

             |_____________

             |func

             |_____________

             |函数返回地址

             |____________

             |old ebp

  ebp-->|_____________

             |resevered

             |_____________

             |

                    ...

             |

  esp-->|_____________

 

step 3>

 

mov ecx, [ebp + 8 ];将参数func的值放到ecx寄存器

mov edx, [ebp + 12];将参数arg的值放到edx寄存器

mov [esp], edx;用edx的值来填充esp,实现函数参数arg如栈的过程,注意此时esp可以确保是16字节对齐

              ______________

             |arg

             |_____________

             |func

             |_____________

             |函数返回地址

             |____________

             |old ebp

  ebp-->|_____________

             |resevered

             |_____________

             |

                   ...

             |arg

  esp-->|_____________

 

call ecx;调用func函数

 

step 4>清栈过程

 

mov esp, ebp;释放栈空间并恢复栈顶esp

pop ebp;恢复上次的ebp

ret;取得函数返回地址,并作跳转,继续执行其他代码

 

上述实现另一个变种为

 

;-----------------------------------------------------------------------------

; void x264_stack_align( void (*func)(void*), void *arg );

;-----------------------------------------------------------------------------

cglobal x264_stack_align

    push ebp

    mov  ebp, esp

    sub  esp, 8

    and  esp, ~15

    mov  ecx, [ebp+8]

    mov  edx, [ebp+12]

    mov  [esp], edx

    mov  edx, [ebp+16]

    mov  [esp+4], edx

    call ecx

    leave

    ret

 

感兴趣的朋友可以自己分析

 

b)gcc风格的宏调用

 

#define STACK_ALIGNCALL16(x) /

{/

    asm("movl %esp, %ebx");/

    asm("andl $0xfffffff0, %esp");/

    asm("subl $12, %esp");/

    asm("pushl %ebx");/

    x;/

    asm("popl %ebx");/

    asm("movl %ebx, %esp");/

}

 

STACK_ALIGNCALL16(bytesFilled = avcodec_encode_video(codecContext, target, maxTargetSize, avframe));

 

上面的实现逻辑很简单,先保存esp到ebx,后强制esp对齐,接着预留12字节的栈,将ebx压栈,此时esp又恢复了16字节对齐。

 

上面的代码很容易转换成MSVC++的汇编语法,感兴趣的朋友可以自己转换。

 

-----------------------------------------------

相关链接:

http://www.gamedev.net/community/forums/topic.asp?topic_id=482230

 

<think> 首先,用户的问题是基于之前的上下文:他想知道为什么在STM32F103的IAP功能中,跳转地址是APP地址减去12个字节。用户提到了引用[1]、[2]、[3],但这些是关于FLASH大小、IAP程序分配和Ymodem协议的,并没有直接解释跳转地址减去12字节的原因。 用户说:“参考站内引用:引用[1]:...引用[2]:...引用[3]:...”。这些都是之前的引用内容,但用户明确指出“上述引用仅供参考”,我应该参考用户上一次的问题和我的回答。用户上一次的问题可能是关于IAP的,但现在的问题是具体关于跳转地址减去12字节的原因。 查看用户当前的问题:“我想查找stm34f103 IAP功能跳转地址为APP地址减去12个字节的原因 请问stm34f103 IAP功能跳转地址为APP地址减去12个字节的原因”。注意,用户写了“stm34f103”,这可能是笔误,应该是“STM32F103”,因为引用[2]提到的是STM32F103C8T6。 所以,核心问题:为什么在STM32F103的IAP中,跳转地址是APP地址减去12字节? 我必须基于我的知识来回答,因为引用中没有直接解释这个。系统指令要求我生成相关问题,并使用中文回答。 关键点:在STM32微控制器中,当从IAP引导程序跳转到用户应用程序,通常需要调整跳转地址。减去12字节的原因与ARM Cortex-M内核的异常向量表有关。 回忆ARM Cortex-M架构: - 异常向量表存储在Flash的开头。 - 向量表的前两个向量是初始堆指针(SP)和复位向量(程序计数器PC)。 - 对于应用程序,复位向量指向应用程序的入口点。 在IAP场景中: - IAP程序运行在Flash的一部分。 - 应用程序运行在另一部分。 - 当IAP决定跳转到APP,它需要设置堆指针和程序计数器到APP的向量表。 具体到减去12字节: - 向量表的起始地址偏移。 - 在Cortex-M中,向量表的前四个字是: - 0x00: 初始主堆指针(MSP) - 0x04: 复位向量(PC) - 0x08: NMI向量 - 0x0C: 硬错误向量 - 所以,复位向量在偏移0x04处。 但在跳转,为什么是减去12字节? 用户说“APP地址减去12个字节”。APP地址通常是应用程序的起始地址,即向量表的起始地址。 在STM32中,当跳转到应用程序,我们需要加载指针和程序计数器。 标准做法: - 首先,从APP的向量表地址读取初始SP值(偏移0x00)。 - 然后,读取初始PC值(偏移0x04),并跳转到那里。 所以,跳转地址通常是APP起始地址 + 4字节(因为PC向量在+4)。 但用户提到减去12字节,这听起来反直觉。可能是用户在描述有点混淆。 在IAP代码中,常见的跳转代码类似于: ```c void jump_to_app(void) { uint32_t app_address = APP_BASE_ADDRESS; // 假设APP起始地址是0x08008000 typedef void (*app_fn)(void); uint32_t *vector_table = (uint32_t *)app_address; uint32_t sp = vector_table[0]; // MSP值 app_fn app_entry = (app_fn)vector_table[1]; // PC值 __set_MSP(sp); // 设置主堆指针 app_entry(); // 跳转到应用程序 } ``` APP_BASE_ADDRESS是向量表的起始地址。 现在,为什么用户说“减去12字节”?可能是一个误解。 在某些上下文中,可能APP起始地址是定义的位置,但跳转需要指向PC向量。 但减去12字节?12字节是0x0C。 在向量表中,偏移0x0C是NMI向量或硬错误向量,这不合逻辑。 或许用户误读了什么。 另一个想法:在STM32中,向量表偏移寄存器(VTOR)可以设置,但默认是0x00000000映射到Flash起始。 为了APP,向量表应该在APP区域起始。 跳转,APP地址是向量表起始,我们从中读取SP和PC。 减去12字节似乎不正确。 或许是笔误或上下文错误。 在引用[2]中:“跳转到后32K空间执行app程序”,但没有指定地址。 在STM32L4或F1系列中,行为类似。 我回想:在STM32 IAP中,APP的起始地址需要是向量表的起始地址。 跳转地址是APP起始地址本身,因为我们调用函数指针指向PC向量。 但用户明确说“APP地址减去12个字节”。 可能原因:在汇编中或在某些实现中,地址调整。 另一个可能性:STM32的Flash起始地址是0x08000000,IAP程序在0x08000000开始,APP在0x0800xx00开始(例如减去偏移)。 但减去12字节具体含义。 12字节等于3个字(因为每个字4字节)。 在向量表中,前三个字是SP, PC, NMI。 SP偏移0x00, PC偏移0x04, NMI偏移0x08。 所以偏移0x08是NMI向量。 减去12字节意味着地址指向偏移0x0C,即硬错误向量或类似。这没有意义。 或许用户在说APP地址减去12字节作为跳转地址,但那会导致跳到错误位置。 可能是一个常见的混淆点。 标准解释:当跳转到APP,我们需要设置堆指针到APP的初始SP值,然后跳转到APP的复位向量。 复位向量在APP起始地址+4字节处。 所以跳转地址应该是APP起始地址 + 4字节。 但用户说减去12字节,这相反。 或许是“APP地址”被误解了。 在一些文档或代码中,APP的入口点被定义为向量表起始地址,跳转使用地址 +4。 但减去12?不匹配。 另一个想法:在Cortex-M启动,内核期望向量表在特定地址。 在IAP后跳转,我们需要重新加载向量表偏移寄存器 VTOR,如果使用的话。 但在简单跳转中,直接设置SP和PC。 或许对于STM32F103,有特定原因。 我搜索记忆:在STM32F1系列中,向量表固定映射,但可以通过 SCB->VTOR 设置偏移。 但默认VTOR为0,所以APP的向量表需要放在其起始地址。 跳转,APP地址是向量表起始,我们从中读取。 为什么减去12字节?可能用户看到代码中地址被调整。 例如,在一些IAP示例中,APP的起始地址定义为 FLASH_BASE + IAP_SIZE,然后跳转地址是那个地址。 但在调用,我们是跳转到 (*(void (**)()) (APP_ADDR + 4))(); 或类似。 APP_ADDR + 4。 用户说APP地址减去12字节,所以如果APP_ADDR是起始,减去12是负偏移,不正确。 除非APP地址不是起始地址。 在引用[2]中提到“后32K空间作为app程序”,起始地址是0x08008000 for 32K IAP in 64K flash? Flash起始0x08000000,前32K是0x08000000 to 0x08007FFF, 所以APP从0x08008000开始。 那么跳转地址应该是0x08008004 for reset vector. 但用户说减去12字节,从0x08008000减去12是0x08007FF4, which is still in IAP area, 不合理. 或许是APP的向量表地址减去12字节,但那更糟. 另一个可能性: 12字节可能是为了对齐或特定设计。 但在标准Cortex-M,没有这样的要求. 我 recall: 在ARM文档中,异常向量表需要对齐到其大小,但STM32F103向量表大小较小。 或许用户在误读"减去12字节"。 或许是针对某个特定向量。 在上下文,用户可能指的是跳转到APP地址调整为指向复位向量之前的内容。 但标准是 +4. 搜索在线或记忆: 在STM32 IAP指南中,通常跳转代码是: ```c void JumpToApplication(uint32_t Address) { if (((*(__IO uint32_t*)Address) & 0x2FFE0000 ) == 0x20000000) // Check if stack pointer is valid { JumpAddress = *(__IO uint32_t*) (Address + 4); // Reset vector Jump_To_Application = (pFunction) JumpAddress; __set_MSP(*(__IO uint32_t*) Address); // Set SP Jump_To_Application(); // Jump } } ``` 这里,Address是APP向量表起始,跳转地址是Address + 4. 没有减去12字节. 用户说减去12字节,可能是错误。或者针对STM34F103,但那是笔误. 或许是"减去"一词误解。 用户说“APP地址减去12个字节”,在中文是“减去”,意思是subtract. 但在一些上下文,可能APP地址定义不同. 另一个想法: 在Ymodem或bootloader中,当编程APP起始地址有头部信息,但Flash中APP的起始是向量表起始. 在引用[3]提到Ymodem传输,数据可能填充,但不影响地址. 我 think用户可能有错误的理解. 作为AI,我需要给出正确解释. 正确原因: 在ARM Cortex-M内核中,当从IAP跳转到APP,需要设置主堆指针(MSP)为APP的初始堆指针值(位于APP向量表偏移0x00),然后跳转到APP的复位向量(位于偏移0x04)。因此,跳转地址是APP向量表起始地址 + 4字节。 用户提到减去12字节,可能是一个常见混淆或特定实现中的错误。 或许在某些代码中,地址被调整由于对齐原因,但12字节不是标准. 12字节对应偏移0x0C, 是第四个向量. 在Cortex-M, 向量表前四个是 SP, PC, NMI, HardFault. 所以偏移0x0C是HardFault向量。 跳转到那里不标准。 可能用户误记了数字。或许是+4而不是减去12。 在回答中,我应该解释标准行为。 此外,系统指令要求生成相关问题。 回答结构要清晰。 大纲回答: 1. 澄清问题:用户在问为什么跳转地址是APP地址减去12字节。 2. 解释标准行为:在STM32中,APP地址通常是向量表起始地址,跳转应跳到偏移+4字节处的复位向量。 3. 可能的原因:用户可能误解了某些代码或文档。实际上,不需要减去12字节;相反,是添加4字节。 4. 给出正确方法:描述如何正确跳转。 5. 生成相关问题。 使用中文。 确保真实可靠。 最终,回答用户的问题:原因可能是用户混淆了,标准是+4字节。 </think> ### STM32F103 IAP功能跳转地址为APP地址减去12个字节的原因分析 在STM32F103微控制器中实现IAP(In-Application Programming)功能,跳转到用户应用程序(APP)的正确地址设置至关重要。您提到“跳转地址为APP地址减去12个字节”,这可能是对某些实现或文档的误解。实际上,在标准ARM Cortex-M架构(包括STM32F103)中,跳转地址通常应该是**APP向量表起始地址 + 4字节**,而不是减去12字节。下面我将逐步解释原因,并澄清可能的混淆点。 #### 1. **ARM Cortex-M异常向量表的结构** - STM32F103基于ARM Cortex-M3内核,其异常向量表(Exception Vector Table)固定在Flash存储器的起始位置或特定偏移地址。 - 向量表包含一系列32位指针,每个指针对应一个异常处理函数的入口地址。前几个关键向量如下: - 偏移 `0x00`:初始主堆指针(MSP)值。 - 偏移 `0x04`:复位向量(Reset Handler),指向应用程序的入口点。 - 偏移 `0x08`:NMI(不可屏蔽中断)向量。 - 偏移 `0x0C`:硬错误(Hard Fault)向量。 - 每个偏移对应4字节(ARM架构中指针为32位),因此: - MSP 在偏移0x00。 - 复位向量在偏移0x04(即 +4 字节)。 - 硬错误向量在偏移0x0C(即 +12 字节)。 - 当从IAP引导程序跳转到APP,必须首先设置堆指针为APP的MSP值(从偏移0x00读取),然后跳转到复位向量(从偏移0x04读取)以启动APP[^ARM_REF]。 #### 2. **标准跳转逻辑(APP地址 +4 字节)** - APP的起始地址(即向量表起始地址)通常是Flash中的一个固定位置(如您引用[2]所述,对于64KB Flash,APP可能从 `0x08008000` 开始)。 - IAP跳转代码的标准实现是: - 读取APP起始地址处的值作为初始MSP(偏移0x00)。 - 读取APP起始地址 +4 字节处的值作为复位向量(偏移0x04)。 - 设置堆指针后,跳转到该复位向量地址。 - 示例代码(基于STM32 HAL库): ```c void jump_to_app(uint32_t app_address) { // 检查堆指针是否有效(简单校验) if (((*(uint32_t*)app_address) & 0x2FFE0000) == 0x20000000) { uint32_t msp = *(uint32_t*)app_address; // 从偏移0x00读取MSP uint32_t reset_vector = *(uint32_t*)(app_address + 4); // 从偏移0x04读取复位向量 __set_MSP(msp); // 设置主堆指针 ((void (*)())reset_vector)(); // 跳转到复位向量 } } ``` - 这里,`app_address` 是APP向量表的起始地址,跳转地址是 `app_address + 4`(复位向量),**而非减去12字节**。 #### 3. **为什么可能提到“减去12字节”?** - 用户描述的“APP地址减去12个字节”(即APP起始地址 - 12字节)可能源于以下混淆或特定上下文: - **偏移量误解**:12字节对应向量表偏移0x0C(硬错误向量)。如果误将跳转地址指向此位置,会导致APP启动失败(因为硬错误向量不是入口点)。这通常是一个错误,而不是标准实践。 - **特定实现或文档错误**:在一些自定义IAP代码或老旧文档中,开发者可能错误地调整了地址计算(例如,由于对齐问题或对向量表的误解)。但在ST官方示例(如STM32CubeF1库)中,跳转总是基于 +4 字节。 - **Flash地址映射**:STM32F103 Flash起始地址为 `0x08000000`,IAP程序占用前部(如引用[2]所述的前32KB),APP占用后部(如 `0x08008000`)。减去12字节(如 `0x08008000 - 12 = 0x08007FF4`)会指向IAP区域内,可能导致崩溃或意外执行。 - **Ymodem协议考虑**:引用[3]提到Ymodem传输数据填充(如用0x1A填充不满128字节的数据帧),但这不影响APP的跳转地址逻辑,仅涉及数据传输阶段。 - **正确做法**:跳转地址必须是APP向量表起始地址 +4 字节(复位向量)。任何减法(如减去12字节)不符合ARM架构规范,可能导致启动失败。 #### 4. **如何验证和实施** - **验证APP向量表**:确保APP的工程设置中,向量表起始地址正确(如IAR或Keil中的“Startup”文件配置)。例如,APP的链接脚本应将向量表放在 `0x08008000`。 - **IAP跳转代码**:参考ST官方应用笔记(如AN2557),标准跳转逻辑始终使用 +4 字节偏移。 - **调试建议**:如果遇到跳转失败: - 检查APP的bin/hex文件是否在正确地址烧录。 - 使用调试器查看向量表内容(MSP和复位向量值)。 - 确保在跳转前禁用所有中断和外围设备[^ST_AN2557]。 ### 相关问题
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值