用VS Code开发Android原生elf

前言

最近接到了一个需求,需要开发一个Android的elf的demo。在Android Studio上折腾了一下,虽然代码提示很完美,但是部署调试不能令我满意(毕竟是拿来开发app的),所以折腾一下VSCode。

下面的配置所必须的插件:C/C++CMake ToolsCodeLLDB

配置导入路径

这里使用的是比较新的NDK(27.2),旧版本的NDK路径可能不一样

首先使用NDK中的clang查询其导入文件的路径aarch64-linux-android22-clang++.cmd -E -x c++ - -v,查看#include <...> search starts here:部分

这里需要需要四个include路径,如下
{ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include
{ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\aarch64-linux-android
{ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\c++\\v1
{ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\lib\\clang\\18\\include

其中include\\aarch64-linux-android是特定架构的头文件,这里如果想要在vscode中可以根据选择的配置来使用不同的头文件,则需要在.vscode目录中创建一个文件c_cpp_properties.json,添加如下内容(注意修改ndk路径)

{
    "configurations": [
        {
            "name": "ARM (32-bit)",
            "includePath": [
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\arm-linux-androideabi",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\c++\\v1",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\lib\\clang\\18\\include"
            ],
            "defines": ["__ARM_ARCH_7A__", "ANDROID"],
            "intelliSenseMode": "linux-clang-arm"
        },
        {
            "name": "ARM64 (64-bit)",
            "includePath": [
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\aarch64-linux-android",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\c++\\v1",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\lib\\clang\\18\\include"
            ],
            "defines": ["__aarch64__", "ANDROID"],
            "intelliSenseMode": "linux-clang-arm64"
        },
        {
            "name": "x86_64 (64-bit)",
            "includePath": [
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\x86_64-linux-android",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\c++\\v1",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\lib\\clang\\18\\include"
            ],
            "defines": ["__x86_64__", "ANDROID"],
            "intelliSenseMode": "linux-clang-x64"
        }
        ,{
            "name": "x86 (32-bit)",
            "includePath": [
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\i686-linux-android",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\c++\\v1",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\lib\\clang\\18\\include"
            ],
            "defines": ["__i386__", "ANDROID"],
            "intelliSenseMode": "linux-clang-x86"
        },
        {
            "name": "RISC-V (64-bit)",
            "includePath": [
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\riscv64-linux-android",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\c++\\v1",
                {ndkdir}\\toolchains\\llvm\\prebuilt\\windows-x86_64\\lib\\clang\\18\\include"
            ],
            "defines": ["__riscv", "__riscv_xlen=64", "ANDROID"],
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

然后就可以在右下角选择不同的架构

file

此时代码提示已经正常了,代码也不会因为找不到头文件报错

CMake与构建

设定CMake路径

首先需要设定cmake的路径,这里使用的是AndroidSDK中的CMake。打开VS Code设置中的工作区(针对当前文件夹的单独设定),搜索找到CMake Path,设定为路径{SDKDir}\cmake\3.31.6\bin\cmake.exe

或者直接在文件夹下的.settings.json中添加一条"cmake.cmakePath": "{SDKDir}\\cmake\\3.31.6\\bin\\cmake.exe",也是一样的效果

CMake配置

首先在项目根目录下创建CMakeList.txt

cmake_minimum_required(VERSION 3.22.1)

# 设置 Android NDK 路径
set(ANDROID_NDK_HOME "path/to/ndk")
# 设置 CMake Toolchain 文件路径
set(CMAKE_TOOLCHAIN_FILE "${ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake")
# 设置编译器路径
set(CMAKE_C_COMPILER "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang.cmd")
set(CMAKE_CXX_COMPILER "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang++.cmd")

# Android 相关配置
set(ANDROID_PLATFORM android-21)  # 最低支持的 API 级别
set(ANDROID_ABI arm64-v8a)  # 目标架构
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

project(vtouchtest)

include_directories(".")

# 添加头文件路径
include_directories(include)
include_directories(${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/18/include) # clang的头文件
include_directories(${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/windows-x86_64/include/c++/4.9.x) # c++标准头文件
include_directories(${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include) # 系统头文件
# 架构特定的头文件
if(ANDROID_ABI STREQUAL "arm64-v8a")
    include_directories(${ANDROID_SYSROOT}/usr/include/aarch64-linux-android)
elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
    include_directories(${ANDROID_SYSROOT}/usr/include/arm-linux-androideabi)
elseif(ANDROID_ABI STREQUAL "x86_64")
    include_directories(${ANDROID_SYSROOT}/usr/include/x86_64-linux-android)
elseif(ANDROID_ABI STREQUAL "x86")
    include_directories(${ANDROID_SYSROOT}/usr/include/i686-linux-android)
elseif(ANDROID_ABI STREQUAL "riscv64")
    include_directories(${ANDROID_SYSROOT}/usr/include/riscv64-linux-android)
endif()

# 收集源文件
file(GLOB_RECURSE SOURCE_FILES "src/*.cpp" "src/*.c")

# 创建可执行文件
add_executable(${CMAKE_PROJECT_NAME} ${SOURCE_FILES})

# 依赖库,按需添加
target_link_libraries(${CMAKE_PROJECT_NAME} "log")
target_link_libraries(${CMAKE_PROJECT_NAME} "z")
target_link_libraries(${CMAKE_PROJECT_NAME} "dl")
target_link_libraries(${CMAKE_PROJECT_NAME} "m")

此时就已经可以在终端中使用CMake进行构建了,命令如下

\path\to\sdk\cmake\3.31.6\bin\cmake.exe -G Ninja -DCMAKE_MAKE_PROGRAM="\path\to\sdk/cmake/3.31.6/bin/ninja.exe" .
\path\to\sdk\cmake\3.31.6\bin\cmake.exe --build .

但是这样并不优雅,所以还是接着配置vscode

这里使用Ctrl+Shift+P打开命令面板,搜索并执行CMake: Add Configure Preset ,输入名称,这会在项目目录下创建一个CMakePresets.json,并生成一个模板配置内容,这里补充CMAKE_MAKE_PROGRAM、CMAKE_TOOLCHAIN_FILE、ANDROID_ABI、ANDROID_PLATFORM,扩展为为如下内容

{
    "name": "android-arm64-debug",
    "displayName": "使用工具链文件配置预设",
    "description": "设置 Ninja 生成器、版本和安装目录",
    "generator": "Ninja",
    "binaryDir": "${sourceDir}/out/build/${presetName}",
    "cacheVariables": {
        "CMAKE_MAKE_PROGRAM": "/Path/To/SDK/cmake/3.31.6/bin/ninja.exe",
        "CMAKE_TOOLCHAIN_FILE": "/Path/To/NDK/build/cmake/android.toolchain.cmake",
        "CMAKE_BUILD_TYPE": "Debug",
        "ANDROID_ABI": "arm64-v8a",
        "ANDROID_PLATFORM": "android-21",
        "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}"
    }
}

此时就可以删掉CMakeList.txt中的set(ANDROID_PLATFORM android-21)set(ANDROID_ABI arm64-v8a)了,通过预设可以设定目标架构

这时在vscode的CMake侧边栏中,配置预设中选择刚才创建的预设

file

这会执行配置操作,也就是上面的\path\to\sdk\cmake\3.31.6\bin\cmake.exe -G Ninja -DCMAKE_MAKE_PROGRAM="\path\to\sdk/cmake/3.31.6/bin/ninja.exe" .操作

然后在生成处点击“生成”,即可编译处目标文件。或者在打开的cpp文件右上角,点击编译活动文件,也是一样的

file

部署与运行

这里说的部署,实际上就是发送到目标设备上,运行。

需要注意的是,受到vscode的限制,没什么比较好的方案在不编写新插件的情况下,在运行人物或者launch的时候支持多设备选择,以及处理多架构部署等等情况。

首先创建一个名为deploy.ps1的辅助脚本用于部署和运行elf,具体如下(其他系统对着改一下就好),放置在项目根目录的tools目录下

param (
    [string]$BinaryPath="",      # 二进制文件路径
    [string]$DeviceTarget="/data/local/tmp",    # 可选: 设备中目标二进制的路径
    [string]$DeviceSerial="", # 可选: 特定设备序列号
    [switch]$Deploy,  # 是否部署
    [switch]$Run,  # 是否后运行
    [switch]$Root,        # 是否使用root运行
)

function Exit-WithError {
    param (
        [string]$Message,
        [int]$ExitCode = 1
    )
    Write-Error $Message
    exit $ExitCode
}

# 获取root权限并返回root方法
function Get-RootAccess {
    param (
        [string]$AdbCmd
    )

    $rootMethod = $null

    # 检查su命令是否可用
    $suCheck = Invoke-Expression "$AdbCmd shell 'which su'"
    if ($suCheck -and $suCheck -ne "" -and $LASTEXITCODE -eq 0) {
        Write-Host "Device has su binary available."
        $rootMethod = "su"
        return $rootMethod
    }

    # 尝试adb root
    Write-Host "Attempting to restart adb as root..."
    Invoke-Expression "$AdbCmd root"
    if ($LASTEXITCODE -eq 0) {
        # 等待adb重新连接
        Start-Sleep -Seconds 2

        # 验证adb是否真正以root运行
        $idCheck = Invoke-Expression "$AdbCmd shell id"
        if ($idCheck -match "uid=0\(root\)") {
            Write-Host "ADB restarted in root mode successfully."
            $rootMethod = "adb_root"
            return $rootMethod
        } else {
            Write-Host "ADB root command didn't grant root privileges."
            return $null
        }
    }

    # 如果两种方法都失败,返回null
    return $null
}

# 使用root权限执行命令
function Invoke-RootCommand {
    param (
        [string]$AdbCmd,
        [string]$RootMethod,
        [string]$Command
    )

    if ($RootMethod -eq "su") {
        # 使用su执行命令
        return Invoke-Expression "$AdbCmd shell `"su -c '$Command'`""
    } 
    elseif ($RootMethod -eq "adb_root") {
        # 直接执行命令(adb已经以root运行)
        return Invoke-Expression "$AdbCmd shell `"$Command`""
    }
    else {
        Exit-WithError "No root method available."
    }
}

# 检查adb是否可用
try {
    Invoke-Expression "adb version"
} catch {
    Exit-WithError "ADB not found in PATH. Please install Android SDK Platform Tools or add it to PATH."
}

if ($Deploy) {
    # 确保参数有效
    if (-not $BinaryPath -or $BinaryPath -eq "") {
        Exit-WithError "Missing binary file path!"
    }

    # 检查二进制文件是否存在
    if (-not (Test-Path $BinaryPath)) {
        Exit-WithError "Binary file does not exist: $BinaryPath"
        exit 1
    }
}

# 构建adb命令
$adbCmd = "adb"
if ($DeviceSerial) {
    $adbCmd = "adb -s $DeviceSerial"
}

# 检查设备连接状态
$deviceCheck = Invoke-Expression "$adbCmd devices" | Select-String "device$"
if (-not $deviceCheck) {
    if ($DeviceSerial) {
        Exit-WithError "Device with serial '$DeviceSerial' not found or not authorized."
    } else {
        Exit-WithError "No connected devices found. Please connect a device or start an emulator."
    }
}

# 检查是否连接了多个设备,但未指定设备序列号
if (-not $DeviceSerial) {
    $deviceCount = @(Invoke-Expression "adb devices" | Select-String "device$").Count
    if ($deviceCount -gt 1) {
        $devicesList = Invoke-Expression "adb devices" | Select-String "device$"
        $devicesInfo = "Connected devices:`n" + ($devicesList -join "`n")
        Exit-WithError "Multiple devices detected but no specific device specified. Please use the -DeviceSerial parameter.`n$devicesInfo"
    }
}

# 提取文件名
$fileName = Split-Path $BinaryPath -Leaf

if ($Deploy) {
    # 执行部署
    Write-Host "Deploying $fileName to device..."
    Invoke-Expression "$adbCmd push `"$BinaryPath`" `"$DeviceTarget/$fileName`""

    if ($LASTEXITCODE -ne 0) {
        Exit-WithError "Deployment failed! Check if target directory is writable." $LASTEXITCODE
    }

    # 设置执行权限
    Write-Host "Setting execution permissions..."
    Invoke-Expression "$adbCmd shell chmod 755 `"$DeviceTarget/$fileName`""

    if ($LASTEXITCODE -ne 0) {
        Exit-WithError "Failed to set execution permissions." $LASTEXITCODE
    }
}

# 是否需要运行
if ($Run) {
    Write-Host "Running application on device..."

    if ($Root) {
        # 获取root权限
        $rootMethod = Get-RootAccess -AdbCmd $adbCmd

        if (-not $rootMethod) {
            Exit-WithError "Device does not appear to be rooted. Cannot run as root."
        }

        Write-Host "Running with root privileges..."
        $command = "cd $DeviceTarget && ./$fileName"
        Invoke-RootCommand -AdbCmd $adbCmd -RootMethod $rootMethod -Command $command
    } else {
        Write-Host "Running with normal privileges..."
        Invoke-Expression "$adbCmd shell `"cd $DeviceTarget && ./$fileName`""
    }

    if ($LASTEXITCODE -ne 0) {
        Exit-WithError "Application execution failed with exit code $LASTEXITCODE." $LASTEXITCODE
    }
}

脚本由将部署和执行功能分开,后面配置起来更加灵活。

然后给CMake添加编译目标,由CMake调用脚本,这样免去了手动指定编译产物的麻烦

在CMakeList.txt下后方添加如下内容,如果需要指定设备则需要把设备代码填充到""中

# 配置文件后缀
if(CMAKE_HOST_WIN32)
    set(SCRIPT_EXT ".ps1")
    set(SCRIPT_CMD powershell)
    set(SCRIPT_ARGS -ExecutionPolicy Bypass -File)
else()
    set(SCRIPT_EXT ".sh") 
    set(SCRIPT_PREFIX "sh")
endif()

# 获取目标文件的路径 - 修复的部分
set(TARGET_NAME ${CMAKE_PROJECT_NAME})  # 确保这是您的实际目标名称

# 仅部署到设备
add_custom_target(deploy
    COMMAND ${SCRIPT_CMD} ${SCRIPT_ARGS} "${CMAKE_CURRENT_SOURCE_DIR}/tools/deploy${SCRIPT_EXT}" 
            "$<TARGET_FILE:${TARGET_NAME}>"
            "/data/local/tmp" 
            "" 
            "-Deploy"
    DEPENDS vtouchtest
    COMMENT "部署到设备(不执行)"
    VERBATIM
)

# 以普通权限执行
add_custom_target(run
    COMMAND ${SCRIPT_CMD} ${SCRIPT_ARGS} "${CMAKE_CURRENT_SOURCE_DIR}/tools/deploy${SCRIPT_EXT}" 
            "$<TARGET_FILE:${TARGET_NAME}>"
            "/data/local/tmp" 
            "" 
            "-Run"
    DEPENDS vtouchtest
    COMMENT "在设备中执行"
    VERBATIM
)

# 以root权限执行
add_custom_target(run_as_root
    COMMAND ${SCRIPT_CMD} ${SCRIPT_ARGS} "${CMAKE_CURRENT_SOURCE_DIR}/tools/deploy${SCRIPT_EXT}" 
            "$<TARGET_FILE:${TARGET_NAME}>"
            "/data/local/tmp" 
            "" 
            "-Run" 
            "-Root"
    DEPENDS vtouchtest
    COMMENT "在设备中以root权限执行"
    VERBATIM
)

最后在.vscode目录中添加一个tasks.json文件,添加上对应的任务即可

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "cmake",
            "label": "CMake: configure",
            "command": "configure",
            "preset": "${command:cmake.activeConfigurePresetName}",
            "detail": "CMake template configure task",
            "group": {
                "kind": "build",
                "isDefault": true
            }
        },
        {
            "type": "cmake",
            "label": "CMake: build",
            "command": "build",
            "preset": "${command:cmake.activeBuildPresetName}",
            "detail": "CMake template build task",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "dependsOn": [
                "CMake: configure"
            ]
        },
        {
            "type": "cmake",
            "label": "CMake: install",
            "command": "install",
            "preset": "${command:cmake.activeBuildPresetName}",
            "detail": "CMake template install task",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "dependsOn": [
                "CMake: build"
            ]
        },
        {
            "type": "cmake",
            "label": "CMake: clean",
            "command": "clean",
            "preset": "${command:cmake.activeBuildPresetName}",
            "detail": "CMake template clean task",
            "group": {
                "kind": "build",
                "isDefault": true
            }
        },
        {
            "type": "cmake",
            "label": "CMake: clean rebuild",
            "command": "cleanRebuild",
            "preset": "${command:cmake.activeBuildPresetName}",
            "detail": "CMake template clean rebuild task",
            "group": {
                "kind": "build",
                "isDefault": true
            }
        },
        {
            "label": "部署",
            "type": "cmake",
            "command": "build",
            "targets": [
                "deploy"
            ],
            "detail": "部署输出项目到目标设备",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "dependsOn": [
                "CMake: build"
            ]
        },
        {
            "label": "仅运行",
            "type": "cmake",
            "command": "build",
            "targets": [
                "run"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        },
        {
            "label": "仅运行 (root)",
            "type": "cmake",
            "command": "build",
            "targets": [
                "run_as_root"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        },
        {
            "label": "构建并运行",
            "type": "cmake",
            "command": "build",
            "targets": [
                "run"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "dependsOn": [
                "部署"
            ]
        },
        {
            "label": "构建并运行 (root)",
            "type": "cmake",
            "command": "build",
            "targets": [
                "run_as_root"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "dependsOn": [
                "部署"
            ]
        }
    ],
}

现在就可以在编写完代码后,通过执行任务(具体来说,Task: Run Task中选择配置的任务),实现从构建到在设备上执行elf,一气呵成。

至于配置launch.json,无论是运行还是调试,都需要lldb,所以放到后面的步骤进行

调试

现在可以运行,就只剩下调试了。

对于调试来说,新版本的ndk只剩下lldb-server,已经不适用gdb了,所以这里也使用lldb-server。

lldv-server启停

首先需要做的就是管理lldb-server的启停。

对于启动来说,整体分为两步,即启动和端口转发。

辅助powershell脚本添加三个参数,同样每个功能单独配置

[switch]$StartLldb,  # 是否开启lldb-server
[switch]$StopLldb,   # 是否停止lldb-server
[int]$LldbPort=8086  # LLDB 端口,默认8086

对于停止来说,无非就是检查ps中有没有lldb-server,有的话干掉他。添加一个函数

# 停止LLDB服务器进程
function Stop-LldbServer {
    param (
        [string]$AdbCmd,
        [bool]$FailOnError = $true
    )

    # 检查lldb-server是否在运行
    $lldbRunning = Invoke-Expression "$AdbCmd shell 'ps | grep lldb-server | grep -v grep'"

    if ($lldbRunning) {
        Write-Host "Terminating lldb-server processes..."

        # 检查lldb-server进程是否属于root
        $isRootProcess = $lldbRunning -match "root"

        # 根据进程所有者选择合适的权限进行终止
        if ($isRootProcess) {
            Write-Host "LLDB server is running as root, using root privileges to stop it..."
            # 获取root权限
            $rootMethod = Get-RootAccess -AdbCmd $AdbCmd

            if (-not $rootMethod) {
                if ($FailOnError) {
                    Exit-WithError "Device does not have root access. Cannot stop root LLDB server."
                } else {
                    Write-Warning "Device does not have root access. Cannot stop root LLDB server."
                    return $false
                }
            }

            # 使用root权限终止进程
            $command = "killall lldb-server 2>/dev/null || true"
            Invoke-RootCommand -AdbCmd $AdbCmd -RootMethod $rootMethod -Command $command
        } else {
            # 普通权限终止进程
            Write-Host "Stopping LLDB server with normal privileges..."
            Invoke-Expression "$AdbCmd shell 'killall lldb-server 2>/dev/null || true'"
        }

        # 验证进程是否真的被终止
        Start-Sleep -Seconds 1
        $stillRunning = Invoke-Expression "$AdbCmd shell 'ps | grep lldb-server | grep -v grep'"
        if ($stillRunning) {
            if ($FailOnError) {
                Exit-WithError "Failed to stop lldb-server processes. Please restart your device or check permissions."
            } else {
                Write-Warning "Failed to stop lldb-server processes. Please restart your device or check permissions."
                return $false
            }
        } else {
            Write-Host "Successfully stopped lldb-server processes."
        }
    } else {
        Write-Host "No running lldb-server processes found."
    }

    return $true
}

这里需要注意的是,如果lldb-server的用户是root,则也需要root用户关闭才行。

然后给关闭参数添加一个功能

if ($StopLldb) {
    Write-Host "Stopping LLDB server..."

    # 停止lldb-server进程
    Stop-LldbServer -AdbCmd $adbCmd

    # 移除端口转发
    Write-Host "Removing port forwarding..."
    Invoke-Expression "$adbCmd forward --remove tcp:$LldbPort 2>/dev/null" | Out-Null

    if ($LASTEXITCODE -ne 0) {
        Exit-WithError "Failed to remove port forwarding." $LASTEXITCODE
    }

    Write-Host "LLDB server successfully stopped."
    exit 0
}

对于开启来说,则需要,根据路径找到lldb-server,并使用设定的端口启动它。再启动之前会首先检查设定的端口是否被占用,并尝试删除现有的端口转发,最终启动lldb。

首先需要一个函数来检查设备端口和本地pc端口是否被占用,是否可以启用lldb-server和转发

# 检查端口是否被占用
function Test-PortInUse {
    param (
        [string]$AdbCmd,
        [int]$Port
    )

    # 在设备上检查端口是否被占用
    $netstatOutput = Invoke-Expression "$AdbCmd shell 'netstat -tunla'"
    $netstatCheck = Write-Output $netstatOutput | Select-String '(?<=\s(?:\[?[\da-fA-F:\.]+]?:|0\.0\.0\.0:))$Port(?=\s|$)'

    if ($netstatCheck) {
        # 端口被占用
        return $true
    }

    # 在主机上检查ADB转发的端口是否被占用
    $portCheck = $null

    # 使用正则表达式匹配本地端口
    $portCheck = Invoke-Expression 'netstat -ano | Select-String -Pattern "(?:TCP|UDP)\s+.*\b$Port\b(?=\s|$)"'

    return ($null -ne $portCheck -and $portCheck -ne "")
}

因为lldb是一个阻塞进程,所以还需要利用powershell的Start-Job非阻塞启动lldb-server,添加一个函数

# 使用PowerShell后台作业非阻塞启动lldb-server
function Start-LldbServer {
    param (
        [string]$AdbCmd,
        [string]$DeviceTarget,
        [int]$Port,
        [bool]$UseRoot = $false
    )

    if ($UseRoot) {
        # 获取root权限
        $rootMethod = Get-RootAccess -AdbCmd $AdbCmd

        if (-not $rootMethod) {
            Exit-WithError "Device is not rooted. Cannot start lldb-server as root."
        }

        Write-Host "Starting lldb-server with root privileges..."
        if ($rootMethod -eq "su") {
            # 使用su运行lldb-server
            $command = "su -c 'cd $DeviceTarget && ./lldb-server platform --server --listen *:$Port > /dev/null 2>&1 &'"
            # 使用PowerShell后台作业启动
            $job = Start-Job -ScriptBlock { 
                param($cmd, $shellcmd)
                & cmd /c "$cmd shell `"$shellcmd`""
            } -ArgumentList $AdbCmd, $command
        } else {
            # 使用adb root运行
            $command = "cd $DeviceTarget && ./lldb-server platform --server --listen *:$Port > /dev/null 2>&1 &"
            # 使用PowerShell后台作业启动
            $job = Start-Job -ScriptBlock { 
                param($cmd, $shellcmd)
                & cmd /c "$cmd shell `"$shellcmd`""
            } -ArgumentList $AdbCmd, $command
        }
    } else {
        # 普通权限运行
        Write-Host "Starting lldb-server with normal privileges..."
        $command = "cd $DeviceTarget && ./lldb-server platform --server --listen *:$Port > /dev/null 2>&1 &"
        # 使用PowerShell后台作业启动
        $job = Start-Job -ScriptBlock { 
            param($cmd, $shellcmd)
            & cmd /c "$cmd shell `"$shellcmd`""
        } -ArgumentList $AdbCmd, $command
    }

    # 给lldb-server启动一点时间
    Start-Sleep -Seconds 1

    # 检查作业状态并清理
    if (Get-Job -Id $job.Id -ErrorAction SilentlyContinue) {
        Write-Host "Cleaning up background job..."
        Remove-Job -Id $job.Id -Force -ErrorAction SilentlyContinue
    }
}

最后完成lldb启动功能

if ($StartLldb) {
    Write-Host "Setting up LLDB server..."

    # 检查设备上是否存在lldb服务器
    $lldbPath = "$DeviceTarget/lldb-server"
    $lldbCheck = Invoke-Expression "$adbCmd shell 'ls $lldbPath 2>/dev/null'"
    $lldbExists = ($LASTEXITCODE -eq 0)

    if (-not $lldbExists) {
        Exit-WithError "lldb-server not found at $lldbPath on device. Please push it first."
    }

    # 端口未被占用,尝试移除现有的端口转发(如果存在)
    Write-Host "Removing any existing port forwarding..."
    try {
        Invoke-Expression "$adbCmd forward --remove tcp:$LldbPort 2>&1" | Out-Null
    } catch {
        # 忽略错误,因为可能没有现有的端口转发
    }

    # 检查设备上端口是否被占用
    Write-Host "Checking if port $LldbPort is available on device..."
    $devicePortInUse = Test-PortInUse -AdbCmd $adbCmd -Port $LldbPort

    if ($devicePortInUse) {
        Exit-WithError "Port $LldbPort is already in use on the device. Cannot start lldb-server. Please choose a different port or stop the process using this port."
    }

    # 设置端口转发
    Write-Host "Setting up port forwarding from localhost:$LldbPort to device:$LldbPort..."
    Invoke-Expression "$adbCmd forward tcp:$LldbPort tcp:$LldbPort"

    if ($LASTEXITCODE -ne 0) {
        Exit-WithError "Failed to set up port forwarding." $LASTEXITCODE
    }

    # 使用非阻塞方式启动lldb-server
    Start-LldbServer -AdbCmd $adbCmd -DeviceTarget $DeviceTarget -Port $LldbPort -UseRoot $Root

    # 检查lldb-server是否成功启动
    Start-Sleep -Seconds 1
    $lldbRunning = Invoke-Expression "$adbCmd shell 'ps | grep lldb-server | grep -v grep'"

    if (-not $lldbRunning) {
        Exit-WithError "lldb-server failed to start properly."
    }

    Write-Host "LLDB server successfully started on port $LldbPort"
    Write-Host "Port forwarding established: localhost:$LldbPort -> device:$LldbPort"
    exit 0
}

给CMakeList添加上最硬的target(其实不加也行,之前是为了方便处理编译输出物路径,但是借助CMake Tool插件添加task更方便),并添加task即可,如下

# 启动lldb-server
add_custom_target(start_lldb
    COMMAND ${SCRIPT_CMD} ${SCRIPT_ARGS} "${CMAKE_CURRENT_SOURCE_DIR}/tools/deploy${SCRIPT_EXT}" 
            "" 
            "/data/local/tmp" 
            "" 
            "-StartLldb"
    COMMENT "Starting LLDB server on device"
    VERBATIM
)

# root权限启动lldb-server
add_custom_target(start_lldb_root
    COMMAND ${SCRIPT_CMD} ${SCRIPT_ARGS} "${CMAKE_CURRENT_SOURCE_DIR}/tools/deploy${SCRIPT_EXT}" 
            "" 
            "/data/local/tmp" 
            "" 
            "-StartLldb"
            "-Root"
    COMMENT "Starting LLDB server with root privileges"
    VERBATIM
)

# 停止lldb-server
add_custom_target(stop_lldb
    COMMAND ${SCRIPT_CMD} ${SCRIPT_ARGS} "${CMAKE_CURRENT_SOURCE_DIR}/tools/deploy${SCRIPT_EXT}" 
            "" 
            "/data/local/tmp" 
            "" 
            "-StopLldb"
    COMMENT "Stopping LLDB server on device"
    VERBATIM
)
        {
            "label": "启动LLDB服务器",
            "type": "cmake",
            "command": "build",
            "targets": [
                "start_lldb"
            ],
            "group": "build",
            "detail": "启动设备上的lldb服务器"
        },
        {
            "label": "启动LLDB服务器 (Root)",
            "type": "cmake",
            "command": "build",
            "targets": [
                "start_lldb_root"
            ],
            "group": "build",
            "detail": "root权限下启动设备上的lldb服务器"
        },
        {
            "label": "停止LLDB服务器",
            "type": "cmake",
            "command": "build",
            "targets": [
                "stop_lldb"
            ],
            "group": "build",
            "detail": "停止设备上的LLDB服务器"
        }

启动和调试

到这一步终于开始配置launch了。因为使用的是lldb,所以这里使用插件codeLLDB。

因为目前这种方案的运行是基于cmake的target的,并且launch也不支持shell命令执行,也没找到办法创建一个空launch使其只执行TASK,所以这里就不创建执行的launch了,这个交给插件做更好。

这里针对调试,需要添加一个task来准备lldb环境。准确来说就是启动lldb-server,以及编译部署好产物。

添加两个task,分别执行构建、部署、lldb-server的停止和启动,非root和root创建两个

        {
            "label": "运行调试准备",
            "dependsOrder": "sequence",
            "dependsOn": [
                "CMake: build",
                "部署",
                "停止LLDB服务器",
                "启动LLDB服务器"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "presentation": {
                "reveal": "silent",
                "revealProblems": "onProblem",
                "close": true
            }
        },
        {
            "label": "运行调试准备 (Root)",
            "dependsOrder": "sequence",
            "dependsOn": [
                "CMake: build",
                "部署",
                "停止LLDB服务器",
                "启动LLDB服务器 (Root)"
            ],
            "group": "build",
            "presentation": {
                "reveal": "silent",
                "revealProblems": "onProblem",
                "close": true
            }
        }

然后利用codelldb插件创建launch,具体如下

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "远程调试",
            "type": "lldb",
            "request": "launch",
            "program": "${command:cmake.launchTargetPath}",
            "cwd": "/data/local/tmp",
            "initCommands": [
                "platform select remote-android",
                "platform connect connect://localhost:28086",
                "platform settings -w /data/local/tmp",
                "settings set target.inherit-env false", // 禁用继承环境变量
            ],
            "stopOnEntry": false,
            "preLaunchTask": "运行调试准备",
            "sourceMap": {
                "/data/local/tmp": "${workspaceFolder}"
            },
            "preTerminateCommands": [
                "process kill",  // 确保进程被正确终止
                "process detach",
                "platform disconnect connect://localhost:28086"
            ],
            "postDebugTask": "停止LLDB服务器",
            "presentation": {
                "hidden": false,
                "group": "调试",
                "order": 1
            }
        },
        {
            "name": "远程调试 (Root)",
            "type": "lldb",
            "request": "launch",
            "program": "${command:cmake.launchTargetPath}",
            "cwd": "/data/local/tmp",
            "initCommands": [
                "platform select remote-android",
                "platform connect connect://localhost:28086",
                "platform settings -w /data/local/tmp",
                "settings set target.inherit-env false" // 禁用继承环境变量
            ],
            "stopOnEntry": false,
            "preLaunchTask": "运行调试准备 (Root)",
            "sourceMap": {
                "/data/local/tmp": "${workspaceFolder}"
            },
            "preTerminateCommands": [
                "process kill",  // 确保进程被正确终止
                "process detach",
                "platform disconnect connect://localhost:28086"
            ],
            "postDebugTask": "停止LLDB服务器",
            "presentation": {
                "hidden": false,
                "group": "调试",
                "order": 2
            }
        }
    ]
}

需要注意的是,cwd需要填写的是目标设备的路径,而不是本地的。并且受到codelldb插件限制,其attach似乎只能选择当前电脑本地的pid,所以就只添加了调试。

至此,可以在vscode中愉快的开发Android的elf了。

后记

完整环境见WangONC / VSCode-Android-ELF-Template

配置到最后发现,配置文件的局限性很大,想变得更好用一点,自动化的配置更多东西非常困难,所以比较好的选择还是,直接开发一个插件…当作下一步的目标吧

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇