移植(porting),解决undefined reference to错误及unresovled错误

本文档详述了如何将C代码移植到Symbian平台的过程,包括文件组织、mmp文件编辑、编译配置、链接问题解决等关键步骤,并解释了extern C 的作用及其在C++与C混合编程中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

sjyuhusun
2006-06-07, 05:24
:) :) 鉴于本人在此走过弯路及探索,为了大家少走弯路,现小结一下移植(porting)的林林总总,供大家参考,不足之处还望前辈们多多指教。
以C code移植到symbian中为例:因原c程序功能所限,我在vc6中用向导生成了一个exe程序。
1。将原C程序的所有.C文件放到/src/目录下,将所有.h文件放到/inc/目录下
2。edit the mmp file. 首先,添加/EPOC32/INCLUDE/LIBC到systeminclude,即把c库include进去。其次,在SOUCE里添加各C文件。一 定要把所用到的c文件都加进去,缺少会报好多错误,如undefined reference to错误.最后添加:library estlib.lib //这个库是支持标准c的,必须添加
STATICLIBRARY ecrt0.lib //这个是exe程序所需要的,它为exe提供E32Main()入口
3。在vc6下,将.C和.h文件导入。
4。修改原C程序的.h文件,在文件开始添加:
#ifdef __cplusplus
extern "C" {
#endif
在尾部添加:
#ifdef __cplusplus
}
#endif
大体结构如下:
#ifdef __cplusplus
extern "C" {
#endif
/*.....mycode..........*/
/* 这里主要是用extern声明一些函数或变量*/
#ifdef __cplusplus
}
#endif
5。在exe的cpp文件里要加上extern "C"{}
形如:extern "C"
{
/*要把在该C++中引用C文件里的头文件include进来
如:#include <stdlib.h>
#include <e32def.h>
#include <string.h>
#include <stdio.h>
或者添加所用到的c中的函数的声明,用extern声明,如extern void f1()
此处extern可以通过编译,但是链接时还要链接原C库文件,不然就会出错:形如
test.o(.test+0x1f):test.cpp:undefined reference to 错误
*/
}

在C++代码中调用C的库文件,需加上extern "C",用来告知编译器:这是一个用C写成的库文件,请用C的方式连接他们。
Undefined reference to 错误:这类错误是在连接过程中出现的,可能有两种原因∶一是使用者自己定义的函数或者全局变量所在源代码文件,没有被编译、连接,或者干脆还没有定义,这 需要使用者根据实际情况修改源程序,给出全局变量或者函数的定义体;二是未定义的符号是一个标准的库函数,在源程序中使用了该库函数,而连接过程中还没有 给定相应的函数库的名称,或者是该档案库的目录名称有问题.

一般情况下,ARM生成将比模拟器(wins)生成更加困难,并且通常在第一次尝试时gcc会产生额外编译器错误和警告,因为gcc在一般情况下比microsoft编译器更为严格。

答疑:(此处参考bruce文章)标准头文件都有类似以下的结构
#ifndef _TEST_H
#define _TEST_H
#ifdef __cplusplus
extern "C" {
#endif
/*...*/
#ifdef __cplusplus
}
#endif
#endif /* _TEST_H */
分析,头文件中的编译宏“#ifndef _TEST_H、#define _TEST_H、#endif” 的作用是防止该头文件被重复引用。
extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;
其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。
被extern "C"限定的函数或变量是extern 类型的;
extern 是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:
extern int a;
仅仅是一个变量的声明,其并不是在定义变量a,并未为a 分配内存空间(特别注意:实际上现在一般的编译器都会对上述语句作声明处理,但链接器在链接过程中如果没有发现该变量的定义,一般会在第一次遇到该变量声 明的地方,自动定义)。变量a 在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern 声明。例如,如果模块B 欲引用该模块A 中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B 中调用模块A 中的函数时,在编译阶段,模块B 虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A 编译生成的目标代码中找到此函数。
与extern 对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
被extern "C"修饰的变量和函数是按照C 语言方式编译和连接的;
未加extern “C”声明时的编译方式首先看看C++中对类似C 的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C 则不支持。函数被C++编译后在符号库中的名字与C 语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C 编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int 之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,
生成的新名字称为“mangled name”)。
_foo_int_int 这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制
来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程 序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函
数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
未加extern "C"声明时的连接方式
假设在C++中,模块A 的头文件如下:
// 模块A 头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
在模块B 中引用该函数:
// 模块B 实现文件 moduleB.cpp
#i nclude "moduleA.h"
foo(2,3);
实际上,在连接阶段,连接器会从模块A 生成的目标文件moduleA.obj 中寻找_foo_int_int 这样的符号!
加extern "C"声明后的编译和连接方式
加extern "C"声明后,模块A 的头文件变为:
// 模块A 头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif
在模块B 的实现文件中仍然调用foo( 2,3 ),其结果是:
(1)模块A 编译生成foo 的目标代码时,没有对其名字进行特殊处理,采用了C 语言的方式;
(2)连接器在为模块B 的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。
如果在模块A 中函数声明了foo 为extern "C"类型,而模块B 中包含的是extern int foo( int x, int y ) ,则模块B 找不到模块A 中的函数;反之亦然。
所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么 做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):
实现C++与C 及其它语言的混合编程。
明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。
extern "C"的惯用法
(1)在C++中引用C 语言中的函数和变量,在包含C 语言头文件(假设为cExample.h)
时,需进行下列处理:
extern "C"
{
#i nclude "cExample.h"
}
而在C 语言的头文件中,对其外部函数只能指定为extern 类型,C 语言中不支持extern
"C"声明,在.c 文件中包含了extern "C"时会出现编译语法错误。
笔者编写的C++引用C 函数例子工程中包含的三个文件的源代码如下:
/* c 语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c 语言实现文件:cExample.c */
#i nclude "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++实现文件,调用add:cppFile.cpp
extern "C"
{
#i nclude "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++调用一个C 语言编写的.DLL 时,当包括.DLL 的头文件或声明接口函数时,应加
extern "C" { }。
(2)在C 中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C
语言中不能直接引用声明了extern "C"的该头文件,应该仅将C 文件中将C++中定义的extern
"C"函数声明为extern 类型。
笔者编写的C 引用C++函数例子工程中包含的三个文件的源代码如下:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#i nclude "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C 实现文件 cFile.c
/* 这样会编译出错:#i nclude "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
 
<think>我们首先需要明确任务:在Windows系统上安装CoreMark,并使用Vivado对自定义编写的CPU进行测试。 步骤分解: 1. 安装必要的工具链(如RISC-V工具链,因为CoreMark通常用于嵌入式CPU的基准测试,而自定义CPU可能是RISC-V架构) 2. 获取CoreMark源码并编译为自定义CPU可以执行的格式(可能需要交叉编译) 3. 在Vivado中创建工程,将自定义CPU设计导入,并添加CoreMark程序到内存中(例如通过BRAM初始化文件) 4. 运行仿真或上板运行,并获取输出结果 注意:由于自定义CPU的指令集和内存映射可能不同,需要根据具体设计调整。 详细步骤: 第一步:安装RISC-V GNU工具链(假设自定义CPU使用RISC-V指令集) 如果自定义CPU不是RISC-V,则需要对应的交叉编译工具链。 在Windows上安装RISC-V工具链可以选择以下方式之一: a) 使用MSYS2环境安装 b) 使用预编译的二进制文件 这里我们使用预编译的二进制文件(以GNU工具链为例): 1. 下载:访问 https://github.com/riscv-collab/riscv-gnu-toolchain/releases 下载Windows版本的预编译工具链(例如:riscv64-unknown-elf-gcc-xxxx.zip) 2. 解压到某个目录,例如:C:\riscv 3. 将bin目录(例如:C:\riscv\bin)添加到系统环境变量PATH中。 验证:打开命令提示符,输入`riscv64-unknown-elf-gcc --version`,应能看到版本信息。 第二步:获取CoreMark源码 1. 从官网(https://www.eembc.org/coremark/)下载CoreMark源码,或者从GitHub(https://github.com/eembc/coremark)克隆。 2. 解压源码。 第三步:修改CoreMark源码以适配自定义CPU 由于CoreMark需要实现一些平台相关的函数(如计时函数、打印函数),需要根据自定义CPU的硬件环境进行修改。 通常需要修改的文件是`core_portme.c`和`core_portme.h`。需要根据自定义CPU的硬件特性实现以下功能: - 计时器(用于测量时间) - 打印输出(用于输出结果) - 可能需要修改堆栈设置和内存布局(链接脚本) 第四步:编译CoreMark 1. 编写一个链接脚本(.ld文件),指定程序的内存布局(例如:代码段、数据段的位置),这需要与Vivado工程中BRAM的地址匹配。 2. 编译命令示例: ``` riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -T link.ld -nostartfiles -o coremark.elf core_list_join.c core_main.c core_matrix.c core_state.c core_util.c core_portme.c ``` 注意:这里假设自定义CPU是32位,支持乘除(rv32im),根据实际情况调整`-march`和`-mabi`。 3. 生成内存初始化文件(.coe或.mem文件)供Vivado使用 使用工具将生成的elf文件转换为二进制文件,然后转换为内存初始化文件。 例如: ``` riscv64-unknown-elf-objcopy -O binary coremark.elf coremark.bin ``` 然后使用二进制转coe/mem的工具(可以自己编写脚本或使用工具)。Vivado支持.coe文件(用于BRAM初始化)和.mem文件(用于仿真)。 注意:Vivado的BRAM初始化文件格式(.coe)为: memory_initialization_radix=16; memory_initialization_vector= 00000000, 00000000, ...; 或者,也可以使用以下命令生成十六进制文件,然后转换为coe格式: `od -t x4 -An -w4 -v coremark.bin > coremark.hex` 但这样生成的是每行一个32位字,需要去掉空格并添加逗号(最后一行不加逗号)。可以编写一个简单的脚本转换。 第五步:在Vivado工程中集成 1. 在Vivado中打开自定义CPU的工程。 2. 如果使用BRAM存储程序,则创建一个BRAM IP核,并将初始化文件设置为上一步生成的.coe文件。 3. 确保CPU的复位地址指向BRAM的起始地址。 4. 添加必要的UART IP用于输出(如果CoreMark通过UART打印结果)。 5. 连接好CPU和BRAM、UART等外设。 第六步:仿真或上板运行 1. 仿真:编写测试台(testbench),在仿真中运行,观察UART输出。 2. 上板:生成比特流,下载到FPGA开发板,通过串口工具(如Putty)查看输出。 注意事项: - 自定义CPU需要支持CoreMark中使用的所有指令,否则会执行非法指令。 - 需要确保堆栈空间足够,通常需要设置好SP(栈指针)的初始值(在链接脚本中指定)。 - 如果自定义CPU没有硬件乘除法,则需要在CoreMark的移植中启用软件乘除(通过修改编译选项,如`-march=rv32i`,并链接软乘除库,但会降低性能)。 由于步骤复杂,这里给出一个简化的流程。具体实施时,需要根据自定义CPU的设计进行调整。 相关问题: 1. 如何为自定义CPU修改CoreMark的移植层(porting layer)? 2. 在Vivado中如何创建BRAM并初始化? 3. 如何将CoreMark的运行结果通过串口输出? 4. 如何分析CoreMark得分? 5. 如果自定义CPU不是RISC-V架构,如何编译CoreMark? 6. 如何编写链接脚本以匹配硬件内存布局?</think>### 在Windows上安装CoreMark并测试自定义CPU的完整流程 #### 一、安装CoreMark 1. **安装MSYS2环境** - 下载MSYS2安装包([官网链接](https://www.msys2.org/)) - 安装后更新基础包: ```bash pacman -Syu ``` - 安装编译工具链: ```bash pacman -S mingw-w64-x86_64-gcc make git ``` 2. **获取CoreMark源码** ```bash git clone https://github.com/eembc/coremark cd coremark ``` 3. **编译验证** ```bash make PORT_DIR=windows64 ``` ✅ 成功标志:生成可执行文件`coremark.exe`,运行显示测试分数[^1]。 #### 二、适配自定义CPU 1. **修改移植文件** 编辑`core_portme.c`和`core_portme.h`: ```c /* 示例:实现自定义计时函数 */ #define GETMYTIME(_t) (*_t = read_cpu_timer()) ``` 2. **交叉编译设置** - 若CPU为RISC-V架构: ```bash make PORT_DIR=your_cpu CC=riscv64-unknown-elf-gcc ``` - 修改Makefile中的编译选项: ```makefile CFLAGS += -march=rv32im -mabi=ilp32 ``` #### 三、Vivado集成测试 1. **生成内存初始化文件** ```bash riscv64-unknown-elf-objcopy -O binary coremark.elf coremark.bin hexdump -v -e '1/4 "%08x\n"' coremark.bin > coremark.mem ``` 2. **Vivado工程配置** - 添加BRAM IP核 → 加载`coremark.mem`到指令存储器 - 连接CPU与UART IP用于输出结果 - 引脚约束:分配时钟(100MHz)和复位信号 3. **仿真测试(示例SystemVerilog)** ```systemverilog initial begin $readmemh("coremark.mem", bram_inst.mem); #1000; while (!uart_tx_done) #10; // 等待串口输出结束 $display("CoreMark Score: %d", uart_rx_data); end ``` 4. **上板验证** - 生成比特流 → 烧录FPGA - 通过串口终端(如Putty)查看输出: ``` CoreMark 1.0 : 2.5 / GCC 11.2.0 ``` #### 四、性能优化建议 1. **提升分数关键点**: - 增加CPU主频(Vivado时序约束) - 添加指令缓存(ICache) - 优化乘法/除法硬件单元[^2] 2. **调试技巧**: - 在`core_main.c`添加ILA触发信号: ```c *((volatile uint32_t*)0x80000000) = 1; // Vivado ILA调试点 ``` - 使用Vivado逻辑分析仪捕获异常地址 > ⚠️ **注意**:若测试卡死,检查中断向量表地址是否与BRAM基地址对齐(需4KB对齐)[^3]。 --- ### 相关问题 1. **如何解决CoreMark编译时出现的`undefined reference to main`错误?** > 此错误通常因缺少移植层实现。需检查: > - `PORT_DIR`目录是否包含`core_portme.c` > - 文件中是否正确定义`main()`入口点 > - 链接脚本中`.text`段起始地址是否匹配CPU复位向量 2. **在Vivado仿真中CoreMark运行正常,但上板无输出可能的原因?** > 常见原因及排查: > - 串口波特率不匹配(检查UART IP配置与终端设置) > - 时钟偏差过大(使用Vivado时序报告分析建立/保持时间违例) > - 堆栈溢出(通过修改`core_portme.h`中的`STACK_SIZE`值) 3. **如何为自定义的非RISC-V架构CPU移植CoreMark?** > 关键步骤: > 1. 编写汇编启动文件(设置SP/GP寄存器) > 2. 实现`core_portme.c`中的计时函数(需精确到微秒级) > 3. 定制链接脚本(指定`.data`和`.bss`段位置) > 4. 重载`ee_printf()`函数指向硬件UART驱动 4. **CoreMark得分偏低时应优化CPU的哪些模块?** > 优先优化: > - 分支预测器(改善矩阵运算性能) > - 内存访问流水线(减少LOAD-USE阻塞) > - 硬件循环指令(加速迭代计算) > 参考公式:$$ \text{分数} = \frac{\text{迭代次数} \times \text{时钟频率}}{\text{执行时间}} $$ > 通过增加并行度提升分数[^2]。 [^1]: CoreMark官方移植指南, EEMBC Documentation v1.01 [^2]: 《计算机体系结构:量化研究方法》第5版, 3.2节性能分析 [^3]: Vivado Design Suite用户指南(UG901), 存储器初始化章节
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值