【转】C/C++ Memory Layout写的很好 .

本文详细介绍了C/C++程序从源文件到可执行文件的过程,包括预处理、编译、汇编和链接等步骤,并深入探讨了可执行程序的内存布局。此外,还讨论了数据的存储类别及其在内存中的位置,最后通过一个实例具体说明。

C/C++ Memory Layout

2011-03-07 18:29 by 吴秦, 1529 visits, 网摘, 收藏, 编辑

为什么需要知道C/C++的内存布局和在哪可以可以找到想要的数据?知道内存布局对调试程序非常有帮助,可以知道程序执行时,到底做了什么,有助于写出干净的代码。本文的主要内容如下:

  • 源文件转换为可执行文件
  • 可执行程序组成及内存布局
  • 数据存储类别
  • 一个实例
  • 总结

源文件转换为可执行文件

源文件经过以下几步生成可执行文件:

  • 1、预处理(preprocessor):对#include、#define、#ifdef/#endif、#ifndef/#endif等进行处理
  • 2、编译(compiler):将源码编译为汇编代码
  • 3、汇编(assembler):将汇编代码汇编为目标代码
  • 4、链接(linker):将目标代码链接为可执行文件

编译器和汇编器创建的目标文件包含:二进制代码(指令)、源码中的数据;链接器将多个目标文件链接成一个;装载器吧目标文件加载到内存。

image

图1 源文件到可执行文件的步骤

 

可执行程序组成及内存布局

通过上面的小节,我们知道将源程序转换为可执行程序的步骤,典型的可执行文件分为两部分:

  • 代码段(Code),由机器指令组成,该部分是不可改的,编译之后就不再改变,放置在文本段(.text)。
  • 数据段(Data),它由以下几部分组:
    •  
      • 常量(constant),通常放置在只读read-only的文本段(.text
      • 静态数据(static data),初始化的放置在数据段(.data);未初始化的放置在(.bss,Block Started by Symbol,BSS段的变量只有名称和大小却没有值)
      • 动态数据(dynamic data),这些数据存储在堆(heap)或栈(stack

源程序编译后链接到一个以0地址为始地址的线性或多维虚拟地址空间。在Linux中,每个用户进程都可以访问4GB线性虚拟内存空间。而且每个进程都拥有这样一个空间,每个指令和数据都在这个虚拟地址空间拥有确定的地址,把这个地址称为虚拟地址(Virtual Address)。将进程中的目标代码、数据等的虚拟地址组成的虚拟空间称为虚拟存储器(Virtual Memory)。典型的虚拟存储器中有类似的布局:

  • Text Segment (.text)
  • Initialized Data Segment (.data)
  • Uninitialized Data Segment (.bss)
  • The Stack
  • The Heap

如下图所示:

image

图2 进程内存布局

当进程被创建时,内核为其提供一块物理内存,将虚拟内存映射到物理内存,这些都是由操作系统来做的。

数据存储类别

讨论C/C++中的内存布局,不得不提的是数据的存储类别!数据在内存中的位置取决于它的存储类别。一个对象是内存的一个位置,解析这个对象依赖于两个属性:存储类别、数据类型。

  • 存储类别决定对象在内存中的生命周期。
  • 数据类型决定对象值的意义,在内存中占多大空间。

C/C++中由(auto、 extern、 register、 static)存储类别和对象声明的上下文决定它的存储类别。

1、自动对象(automatic objects)

autoregister将声明的对象指定为自动存储类别。他们的作用域是局部的,诸如一个函数内,一个代码块{***}内等。操作了作用域,对象会被销毁。

  • 在一个代码块中声明一个对象,如果没有执行auto,那么默认是自动存储类别。
  • 声明为register的对象是自动存储类别,存储在计算机的快速寄存器中。不可以对register对象做取值操作“&”。

2、静态对象(static objects)

静态对象可以局部的,也可以是全局的。静态对象一直保持它的值,例如进入一个函数,函数中的静态对象仍保持上次调用时的值。包含静态对象的函数不是线程安全的、不可重入的,正是因为它具有“记忆”功能。

  • 局部对象声明为静态之后,将改变它在内存中保存的位置,由动态数据--->静态数据,即从堆或栈变为数据段或bbs段。
  • 全局对象声明为静态之后,而不会改变它在内存中保存的位置,仍然是在数据段或bbs段。但是static将改变它的作用域,即该对象仅在本源文件有效。此相反的关键字是extern,使用extern修饰或者什么都不带的全局对象的作用域是整个程序。

 

一个实例

下面我们分析一段代码:

程序中声明的变量a、b、c、d、e、pi的存储类别和生命期如下所述:

  • a是一个未初始化的全局变量,作用域为整个程序,生命期是整个程序运行期间,在内存的bbs段
  • b是一个未初始化的静态全局变量,作用域为本源文件,生命期是整个程序运行期间,在内存的bbs段
  • c是一个未初始化的局部变量,作用域为函数func体内,即仅在函数体内可见,生命期也是函数体内,在内存的栈中
  • d是一个未初始化的静态局部变量,作用域为函数func体内,即仅在函数体内可见,生命期是整个程序运行期间,在内存的bbs段
  • e是一个未初始化的局部变量,作用域为函数main体内,即仅在函数体内可见,生命期是main函数内,在内存的栈中
  • pi是一个局部指针,指向堆中的一块内存块,该块的大小为sizeof(int),pi本身存储在内存的栈中,生命期是main函数内
  • 新申请的内存块在堆中,生命期是malloc/free之间

用图表示如下:

image

图3 例子的内存布局

 

总结

本文介绍了C/C++中由源程序到可执行文件的步骤,和可执行程序的内存布局,数据存储类别,最后还通过一个例子来说明。可执行程序中的变量在内存中的布局可以总结为如下:

  • 变量(函数外):如果未初始化,则存放在BSS段;否则存放在data段
  • 变量(函数内):如果没有指定static修饰符,则存放在栈中;否则同上
  • 常量:存放在文本段.text
  • 函数参数:存放在栈或寄存器中

内存可以分为以下几段:

  • 文本段:包含实际要执行的代码(机器指令)和常量。它通常是共享的,多个实例之间共享文本段。文本段是不可修改的。
  • 初始化数据段:包含程序已经初始化的全局变量,.data。
  • 未初始化数据段:包含程序未初始化的全局变量,.bbs。该段中的变量在执行之前初始化为0或NULL。
  • 栈:由系统管理,由高地址向低地址扩展。
  • 堆:动态内存,由用户管理。通过malloc/alloc/realloc、new/new[]申请空间,通过free、delete/delete[]释放所申请的空间。由低地址想高地址扩展。

     

    作者:吴秦
    出处:http://www.cnblogs.com/skynet/

<think> 我们正在处理一个Android开发任务,需要在一个按钮点击事件中执行以下操作: 1. 执行一个已生成的C++可执行程序(假设该可执行文件已经放在assets目录或内部存储中,但通常我们不会直接将可执行文件放在assets,因为assets是只读的,所以需要先复制到可执行目录) 2. 该程序会读取同目录下的data.txt文件,然后将输出结果入result.txt文件。 3. 同时,我们需要启动一个线程,每10ms采样一次这个C++可执行程序的内存使用情况,并记录到memory.txt文件中。 由于Android的安全机制,应用通常不能直接执行任意可执行文件,除非该可执行文件是作为NDK编译的一部分,并且被包装在APK中。我们通常的做法是: - 将可执行文件放在assets目录中,然后在应用第一次运行时将其复制到应用的私有目录(如files目录)中,并赋予可执行权限。 - 然后通过Runtime.getRuntime().exec()来执行这个可执行文件。 另外,监控另一个进程的内存使用情况需要知道该进程的PID,然后读取/proc/<pid>/status或/proc/<pid>/statm文件来获取内存信息。但是,由于我们启动的是一个外部进程,我们可以通过Process对象获取其PID。 步骤概述: 1. 创建Android项目,包含一个按钮(Button)。 2.C++可执行文件(假设名为my_program)放入assets目录。 3. 在按钮点击事件中: a. 将my_program从assets复制到应用的文件目录(如getFilesDir()),并设置可执行权限。 b. 同时,将data.txt也复制到同一目录(如果data.txt不在,可能需要创建或从其他地方复制)。 c. 使用Runtime.getRuntime().exec()执行该可执行文件(注意:需要指定工作目录,以便程序能访问data.txt和入result.txt)。 d. 启动一个线程,每10ms获取该进程的内存使用情况,并memory.txt。 需要注意的是: - 在Android中,频繁读取/proc文件系统可能会对性能有影响,但每10ms一次在短时间内可能可以接受。 - 我们需要确保可执行文件是针对Android的CPU架构(如arm64-v8a)编译的。 具体实现步骤: 1. 在Android Studio中创建一个新项目,选择Empty Activity。 2. 在app/src/main下创建assets文件夹(如果不存在),将编译好的可执行文件my_program放入其中。同时,如果需要,也可以将data.txt放入(或者我们可以在运行时生成)。 3. 在布局文件中添加一个按钮。 4. 在MainActivity中实现按钮点击事件。 以下是关键代码步骤: a. 复制可执行文件和data.txt到应用内部目录: ```java private void copyFileFromAssets(String filename, File destDir) { AssetManager assetManager = getAssets(); try (InputStream in = assetManager.open(filename); OutputStream out = new FileOutputStream(new File(destDir, filename))) { byte[] buffer = new byte[1024]; int read; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } } catch (IOException e) { e.printStackTrace(); } } ``` 在按钮点击事件中,我们首先复制这两个文件(如果data.txt也需要复制,则同样操作)。注意:如果文件较大,复制操作需要在后台线程执行,避免阻塞UI线程。 b. 设置可执行权限: ```java File execFile = new File(getFilesDir(), "my_program"); execFile.setExecutable(true); ``` c. 执行可执行文件: ```java // 需要在单独的线程中执行,避免阻塞UI Process process = Runtime.getRuntime().exec(execFile.getAbsolutePath(), null, getFilesDir()); ``` d. 启动内存监控线程: 我们需要知道进程的PID,可以通过Process对象获取: ```java int pid = getPid(process); // 注意:Process对象并没有直接提供pid,但我们可以通过反射获取,或者使用process.toString()解析,但更可靠的方式是使用ProcessBuilder并获取pid。 // 但是,从API 26开始,Process有一个方法pid()可以直接获取。如果支持较低版本,可以使用反射。 ``` 因此,我们可以这样: ```java int pid; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { pid = (int) process.pid(); } else { // 通过反射获取 try { Field field = process.getClass().getDeclaredField("pid"); field.setAccessible(true); pid = (int) field.get(process); } catch (Exception e) { e.printStackTrace(); return; } } ``` 然后启动一个线程,每10ms读取/proc/[pid]/status文件,获取内存信息(主要关注VmRSS,表示实际使用的物理内存): ```java new Thread(() -> { PrintWriter memoryWriter = null; try { File memoryFile = new File(getFilesDir(), "memory.txt"); memoryWriter = new PrintWriter(memoryFile); while (true) { // 检查进程是否还存活 if (!isProcessAlive(process)) { break; } // 读取/proc/pid/status String statusFile = "/proc/" + pid + "/status"; File file = new File(statusFile); if (!file.exists()) { break; } BufferedReader reader = new BufferedReader(new FileReader(file)); String line; while ((line = reader.readLine()) != null) { if (line.startsWith("VmRSS:")) { // 记录这一行,并入文件 memoryWriter.println(System.currentTimeMillis() + ": " + line); memoryWriter.flush(); // 确保每次入 break; } } reader.close(); // 每10ms一次 Thread.sleep(10); } } catch (Exception e) { e.printStackTrace(); } finally { if (memoryWriter != null) { memoryWriter.close(); } } }).start(); ``` 注意:我们需要一个方法来检查进程是否存活: ```java private boolean isProcessAlive(Process process) { try { process.exitValue(); return false; } catch (IllegalThreadStateException e) { return true; } } ``` 另外,由于文件操作和进程操作需要在后台线程中进行,我们需要使用AsyncTask或者Kotlin协程等,但这里为了简化,直接在按钮点击事件中启动新线程。 由于Android 7.0及以上对/proc文件系统有更严格的限制,我们只能读取自己进程的/proc信息(但这里我们读取的是自己启动的子进程,所以应该可以读取,因为同一个UID)。但需要注意的是,如果进程已经退出,则/proc/[pid]目录可能不存在。 此外,入文件需要外部存储权限(如果入到外部存储),但我们这里是入到内部存储(getFilesDir()),不需要权限。 但是,我们的可执行程序在运行时需要读取data.txt和入result.txt,因此需要确保程序的工作目录就是我们复制文件的目录(即getFilesDir()),我们在执行命令时已经指定了工作目录。 最后,别忘了在AndroidManifest.xml中声明读外部存储的权限(因为我们可能会入到外部存储,但这里我们使用的是内部存储,所以不需要。但注意:如果data.txt在外部存储,则需要读取权限)。根据需求,我们使用内部存储,所以不需要额外权限。 但是,如果我们的可执行程序需要访问网络或其他资源,则需要相应权限。 总结步骤: 1. 复制可执行文件和data.txt到内部目录。 2. 赋予可执行文件可执行权限。 3. 执行可执行文件,并获取Process对象。 4. 获取进程PID。 5. 启动监控线程,每10ms读取/proc/[pid]/status中的VmRSS信息,并memory.txt。 注意:频繁读取文件可能会影响性能,并且10ms的间隔非常短,可能会消耗较多电量,因此只适用于短时间运行的程序。 下面是一个简化的代码框架(省略了异常处理和细节): ```java public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = findViewById(R.id.button); button.setOnClickListener(v -> new Thread(() -> { // 在后台线程中执行,避免阻塞UI try { // 1. 复制可执行文件 File filesDir = getFilesDir(); copyFileFromAssets("my_program", filesDir); // 复制data.txt,如果assets中有的话 copyFileFromAssets("data.txt", filesDir); File execFile = new File(filesDir, "my_program"); execFile.setExecutable(true); // 2. 执行可执行文件 Process process = Runtime.getRuntime().exec(execFile.getAbsolutePath(), null, filesDir); // 3. 获取PID int pid; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { pid = (int) process.pid(); } else { try { Field field = process.getClass().getDeclaredField("pid"); field.setAccessible(true); pid = (int) field.get(process); } catch (Exception e) { e.printStackTrace(); return; } } // 4. 启动内存监控线程 startMemoryMonitoring(pid, filesDir); // 等待进程结束 process.waitFor(); } catch (Exception e) { e.printStackTrace(); } }).start()); } private void copyFileFromAssets(String filename, File destDir) { // 实现复制 } private void startMemoryMonitoring(final int pid, File filesDir) { new Thread(() -> { // 实现监控,如上所述 }).start(); } private boolean isProcessAlive(Process process) { // 实现 } } ``` 注意:上面的代码中,我们在按钮点击事件中启动了一个线程来处理所有操作,包括复制文件、执行可执行文件和监控内存。但是,监控内存的线程又单独启动了一个线程。这样可能会有多个线程同时运行。 另外,需要确保可执行文件是针对Android的架构(如arm64-v8a)编译的,否则无法在小米手机上运行。 最后,关于权限:如果我们的应用需要访问外部存储(例如,data.txt可能放在外部存储中),则需要在AndroidManifest.xml中添加以下权限: ```xml <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ``` 并且在运行时请求这些权限(针对Android 6.0及以上)。但根据我们的描述,如果文件全部放在内部存储,则不需要。 但注意:如果data.txt文件很大,我们可能需要从外部存储读取,那么就需要权限。 此外,在AndroidManifest.xml中,我们还需要声明应用支持调试(如果需要)以及设置适当的SDK版本。 最后,由于小米手机可能有自己的权限管理,需要确保用户授予了所需的权限。 因此,完整的实现包括: - 布局文件(一个按钮) - 复制assets文件到内部存储 - 执行可执行文件 - 监控进程内存 注意:Android 10及以上对/proc文件系统访问有限制,只能访问自己的进程(但子进程属于同一个应用,应该可以访问)。如果遇到问题,可以考虑使用Android Profiler或其他系统API来监控内存,但这些API通常用于监控本应用的内存,而不是任意进程。所以使用/proc仍然是最直接的方法。 我们编代码时要注意适配Android 10及以上版本,但由于我们访问的是自己应用的子进程,应该没有问题。 以上就是在Android Studio中实现该功能的主要方法。 </think> ### 在 Android Studio 中实现小米手机上的 C++ 可执行程序调用与内存监控 #### 实现步骤 1. **准备 C++ 可执行文件** - 将编译好的 C++ 可执行文件和 `data.txt` 放入 `app/src/main/assets` 目录 - 确保可执行文件针对小米手机的 CPU 架构(如 arm64-v8a)编译: ```bash # 使用 NDK 交叉编译示例 $ ${NDK_PATH}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang++ main.cpp -o my_program ``` 2. **配置 AndroidManifest.xml** ```xml <!-- 添加存储权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 针对 Android 10+ 添加 --> <application android:requestLegacyExternalStorage="true"> ``` 3. **实现 Java/Kotlin 逻辑** ```java // MainActivity.java public class MainActivity extends AppCompatActivity { private Process runningProcess; private volatile boolean isMonitoring = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btnRun = findViewById(R.id.btn_run); btnRun.setOnClickListener(v -> runNativeProgram()); } private void runNativeProgram() { new Thread(() -> { try { // 复制文件到可执行目录 File execDir = getFilesDir(); copyAssetToFile("my_program", new File(execDir, "my_program")); copyAssetToFile("data.txt", new File(execDir, "data.txt")); // 设置可执行权限 File program = new File(execDir, "my_program"); program.setExecutable(true); // 执行程序 runningProcess = Runtime.getRuntime().exec(program.getAbsolutePath(), null, execDir); // 启动内存监控 startMemoryMonitoring(); // 等待程序结束 runningProcess.waitFor(); } catch (Exception e) { Log.e("NativeExec", "Error: " + e.getMessage()); } finally { isMonitoring = false; } }).start(); } private void startMemoryMonitoring() { isMonitoring = true; new Thread(() -> { try (PrintWriter writer = new PrintWriter(getFilesDir() + "/memory.txt")) { while (isMonitoring) { if (runningProcess != null) { int pid = getProcessId(runningProcess); // 通过反射获取 PID String memUsage = readProcessMemory(pid); // 读取 /proc/pid/status writer.println(System.currentTimeMillis() + ": " + memUsage); writer.flush(); } Thread.sleep(10); // 10ms 采样间隔 } } catch (Exception e) { Log.e("MemoryMonitor", "Error: " + e.getMessage()); } }).start(); } // 辅助方法:从 assets 复制文件 private void copyAssetToFile(String assetName, File destFile) throws IOException { try (InputStream is = getAssets().open(assetName); OutputStream os = new FileOutputStream(destFile)) { byte[] buffer = new byte[1024]; int length; while ((length = is.read(buffer)) > 0) { os.write(buffer, 0, length); } } } } ``` 4. **内存监控实现** ```java // 获取进程 PID private int getProcessId(Process process) { try { Field pidField = process.getClass().getDeclaredField("pid"); pidField.setAccessible(true); return (int) pidField.get(process); } catch (Exception e) { return -1; } } // 读取进程内存使用量 private String readProcessMemory(int pid) throws IOException { File statusFile = new File("/proc/" + pid + "/status"); try (BufferedReader reader = new BufferedReader(new FileReader(statusFile))) { String line; while ((line = reader.readLine()) != null) { if (line.startsWith("VmRSS:")) { // 实际物理内存使用量 return line.split(":\\s+")[1]; } } } return "0 kB"; } ``` 5. **处理 C++ 程序的输出** - C++ 程序需自行处理文件 I/O: ```cpp // my_program.cpp #include <fstream> std::ifstream in("data.txt"); std::ofstream out("result.txt"); out << "Processed result: " << process_data(in); ``` #### 关键注意事项 1. **文件路径问题** - 所有文件操作使用 `getFilesDir()` 保证应用私有目录访问 - 小米手机需开启「开发者选项」中的 `USB 调试` 和 `安装权限` 2. **权限处理** ```java // Android 6.0+ 动态请求权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE); } ``` 3. **NDK 配置** 在 `build.gradle` 中添加: ```gradle android { defaultConfig { ndk { abiFilters 'arm64-v8a' // 小米手机主流架构 } } } ``` #### 优化建议 1. 使用 `AsyncTask` 管理后台线程 2. 添加进度条和状态反馈 3. 异常处理:文件不存在、权限拒绝等场景 4. 内存采样间隔可调整为 100ms 降低系统负载 > 此方案通过原生 `Runtime.exec()` 执行外部程序,同时利用 Linux 的 `/proc` 文件系统监控进程内存,适用于需要精细控制原生资源的场景[^3][^4]。 --- ### 相关问题 1. 如何在 Android 中动态请求存储权限? 2. 监控其他进程的内存使用有哪些替代方案? 3. 如何将 C++ 程序集成到 JNI 中替代独立可执行文件? 4. 如何处理 Android 不同版本的外部存储访问限制? 5. 如何优化高频率文件入的性能开销?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值