作者 | 0x1306a94
來源 | https://blog.0x1306a94.com/,點擊閱讀原文查看作者更多文章
mkdir LLVM
cd LLVM
wget https://github.com/llvm/llvm-project/archive/llvmorg-11.0.0.zip
unzip llvmorg-11.0.0.zip
在 LLVM 目錄下,創建一個build.sh文件,拷貝下面內容
#!/usr/bin/env bash
set -e
set -o pipefail
set -u
ROOT_DIR=`pwd`
SOURCE_CODE_DIR=$ROOT_DIR/llvm-project-llvmorg-11.0.0
BUILD_DIR=$ROOT_DIR/build_dir
LLVM_BUILD_DIR=$BUILD_DIR/build_llvm
LLVM_OUT_DIR=$BUILD_DIR/build_llvm_out
CLANG_BUILD_DIR=$BUILD_DIR/build_clang
CLANG_OUT_DIR=$BUILD_DIR/build_clang_out
XCODE_BUILD_DIR=$BUILD_DIR/build_xcode
XCODE_OUT_DIR=$BUILD_DIR/build_xcode_out
function build_llvm() {
echo "generate llvm ninja build config ..."
cd $LLVM_BUILD_DIR
cmake -G "Ninja" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="10.14" \
-DCMAKE_OSX_SYSROOT="macosx" \
-DCMAKE_OSX_ARCHITECTURES='x86_64' \
-DLLVM_TARGETS_TO_BUILD="X86" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$LLVM_OUT_DIR \
$SOURCE_CODE_DIR/llvm
echo "ninja build llvm ..."
ninja install
}
function build_clang() {
echo "generate clang ninja build config ..."
cd $CLANG_BUILD_DIR
cmake -G "Ninja" \
-DCMAKE_PREFIX_PATH=$LLVM_OUT_DIR/lib/cmake/llvm \
-DCMAKE_OSX_DEPLOYMENT_TARGET="10.14" \
-DCMAKE_OSX_SYSROOT="macosx" \
-DCMAKE_OSX_ARCHITECTURES='x86_64' \
-DLLVM_TARGETS_TO_BUILD="X86" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$CLANG_OUT_DIR \
$SOURCE_CODE_DIR/clang
echo "ninja build clang ..."
ninja install
}
function generate_xcode_project() {
cd $XCODE_BUILD_DIR
cmake -G "Xcode" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="10.14" \
-DCMAKE_OSX_SYSROOT="macosx" \
-DCMAKE_OSX_ARCHITECTURES='x86_64' \
-DLLVM_TARGETS_TO_BUILD="X86" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$XCODE_OUT_DIR \
$SOURCE_CODE_DIR/llvm
}
function clear_build() {
echo "clear build dir"
rm -rf $BUILD_DIR
}
function usage() {
echo "Usage:"
echo "build.sh -b -x"
echo "Description:"
echo "-b, build llvm and clang"
echo "-x, generate xcode project"
echo "-c, clear build dir"
exit 0
}
function onCtrlC () {
clear_build
exit 0
}
BUILD_FLAG=0
GENERATE_XCODE_FLAG=0
CLEAR_BUILD_FLAG=0
while getopts 'hbxc' OPT; do
case $OPT in
b) BUILD_FLAG=1;;
x) GENERATE_XCODE_FLAG=1;;
c) CLEAR_BUILD_FLAG=1;;
h) usage;;
?) usage;;
esac
done
if [[ $CLEAR_BUILD_FLAG == 1 ]]; then
clear_build
fi
if [[ $BUILD_FLAG == 1 ]]; then
# trap 'onCtrlC' INT
startTime_s=`date +%s`
if [[ $GENERATE_XCODE_FLAG == 1 ]]; then
mkdir -p $XCODE_BUILD_DIR
generate_xcode_project
else
mkdir -p $LLVM_BUILD_DIR $CLANG_BUILD_DIR
build_llvm
build_clang
fi
endTime_s=`date +%s`
sumTime=$[ $endTime_s - $startTime_s ]
echo "開始: $(date -r $startTime_s +%Y%m%d-%H:%M:%S)"
echo "結束: $(date -r $endTime_s +%Y%m%d-%H:%M:%S)"
hour=$[ $sumTime / 3600 ]
minutes=$[ ($sumTime - ($hour * 3600)) / 60 ]
seconds=$[ ($sumTime - ($hour * 3600)) % 60 ]
echo "耗時:$hour:$minutes:$seconds"
fi
進入llvm-project-llvmorg-11.0.0/llvm/lib/Transforms 目錄
將 Hello 目錄拷貝一份,並重命名為 FunctionCallTime
在 llvm-project-llvmorg-11.0.0/llvm/lib/Transforms/CMakeLists.txt 後面追加一行 add_subdirectory(FunctionCallTime)
在 llvm-project-llvmorg-11.0.0/llvm/lib/Transforms/LLVMBuild.txt subdirectories 後面追加 FunctionCallTime
修改 FunctionCallTime/CMakeLists.txt 為下面內容
# If we don't need RTTI or EH, there's no reason to export anything
# from the hello plugin.
if( NOT LLVM_REQUIRES_RTTI )
if( NOT LLVM_REQUIRES_EH )
set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/FunctionCallTime.exports)
endif()
endif()
if(WIN32 OR CYGWIN)
set(LLVM_LINK_COMPONENTS Core Support)
endif()
set(LLVM_LINK_COMPONENTS Demangle)
add_llvm_library( LLVMFunctionCallTime MODULE BUILDTREE_ONLY
FunctionCallTime.cpp
DEPENDS
intrinsics_gen
PLUGIN_TOOL
opt
)
將 Hello.cpp 重命名為 FunctionCallTime.cpp, Hello.exports 重命名為 FunctionCallTime.exports
將FunctionCallTime.cpp 內容替換為下面
//
// function_call_time.cpp
// function-call-time
//
// Created by king on 2020/12/18.
//
#include <iostream>
#include "llvm/ADT/Statistic.h"
#include "llvm/Demangle/Demangle.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Instruction.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Value.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
using namespace llvm;
namespace {
struct FunctionCallTimePass : public FunctionPass {
static char ID;
static StringRef InsertFuncNamePrefix;
static StringRef BeginFuncName;
static StringRef EndFuncName;
FunctionCallTimePass()
: FunctionPass(ID) {}
bool runOnFunction(Function &F) override {
if (F.empty()) {
return false;
}
std::string annotation = readAnnotate(&F);
auto funcName = F.getName();
// 可以通過 __attribute__((__annotate__(("ignore_appletrace"))))
// 忽略當前函數
if (annotation.length() > 0 &&
StringRef(annotation).contains("ignore_appletrace")) {
errs() << "function-call-time: " << funcName << " 忽略 annotation"
<< "\n";
return false;
}
// Objective-C 方法前面有 \x01
if (funcName.front() == '\x01') {
funcName = funcName.drop_front();
}
if (funcName.startswith("__Z") || funcName.startswith("_Z")) {
// C++ 函數
std::string str = funcName.str();
std::string demangled = demangle(str);
funcName = StringRef(demangled);
}
// 將統計代碼調用過濾掉
if (funcName.startswith("appletrace")) {
return false;
}
// 如果是插樁的函數直接跳過
if (F.getName().startswith(FunctionCallTimePass::InsertFuncNamePrefix)) {
return false;
}
// 只統計 Objective-C 方法調用
if (funcName.startswith("+[") || funcName.startswith("-[")) {
// 2. 插入開始
if (!insertBeginInst(F)) {
return false;
}
// 3. 插入結束
insertEndInst(F);
return false;
}
return false;
}
private:
std::string readAnnotate(Function *f) {
std::string annotation = "";
// Get annotation variable
GlobalVariable *glob =
f->getParent()->getGlobalVariable("llvm.global.annotations");
if (glob != NULL) {
// Get the array
if (ConstantArray *ca = dyn_cast<ConstantArray>(glob->getInitializer())) {
for (unsigned i = 0; i < ca->getNumOperands(); ++i) {
// Get the struct
if (ConstantStruct *structAn =
dyn_cast<ConstantStruct>(ca->getOperand(i))) {
if (ConstantExpr *expr =
dyn_cast<ConstantExpr>(structAn->getOperand(0))) {
// If it's a bitcast we can check if the annotation is concerning
// the current function
if (expr->getOpcode() == Instruction::BitCast &&
expr->getOperand(0) == f) {
ConstantExpr *note =
cast<ConstantExpr>(structAn->getOperand(1));
// If it's a GetElementPtr, that means we found the variable
// containing the annotations
if (note->getOpcode() == Instruction::GetElementPtr) {
if (GlobalVariable *annoteStr =
dyn_cast<GlobalVariable>(note->getOperand(0))) {
if (ConstantDataSequential *data =
dyn_cast<ConstantDataSequential>(
annoteStr->getInitializer())) {
if (data->isString()) {
annotation += data->getAsString().lower() + " ";
}
}
}
}
}
}
}
}
}
}
return annotation;
}
bool insertBeginInst(Function &F) {
// 0.函數最開始的BasicBlock
LLVMContext &context = F.getParent()->getContext();
BasicBlock &BB = F.getEntryBlock();
// 1. 獲取要插入的函數
FunctionCallee beginFun = F.getParent()->getOrInsertFunction(
FunctionCallTimePass::BeginFuncName,
FunctionType::get(Type::getVoidTy(context),
{Type::getInt8PtrTy(context)}, false));
errs() << "function-call-time: " << BB.getParent()->getName() << " begin\n";
// 2. 構造函數
CallInst *inst = nullptr;
IRBuilder<> builder(&BB);
IRBuilder<> callBuilder(context);
Value *name = builder.CreateGlobalStringPtr(BB.getParent()->getName());
inst = callBuilder.CreateCall(beginFun, {name});
if (!inst) {
llvm::errs() << "Create First CallInst Failed\n";
return false;
}
// 3. 獲取函數開始的第一條指令
Instruction *beginInst = dyn_cast<Instruction>(BB.begin());
// 4. 將inst插入
inst->insertBefore(beginInst);
return true;
}
void insertEndInst(Function &F) {
LLVMContext &context = F.getParent()->getContext();
for (Function::iterator I = F.begin(), E = F.end(); I != E; ++I) {
// 函數結尾的BasicBlock
BasicBlock &BB = *I;
for (BasicBlock::iterator I = BB.begin(), E = BB.end(); I != E; ++I) {
ReturnInst *IST = dyn_cast<ReturnInst>(I);
if (!IST)
continue;
// end_func 類型
FunctionType *endFuncType = FunctionType::get(
Type::getVoidTy(context), {Type::getInt8PtrTy(context)}, false);
// end_func
FunctionCallee endFunc = BB.getModule()->getOrInsertFunction(
FunctionCallTimePass::EndFuncName, endFuncType);
// 構造end_func
IRBuilder<> builder(&BB);
IRBuilder<> callBuilder(context);
Value *name = builder.CreateGlobalStringPtr(BB.getParent()->getName());
CallInst *endCI = callBuilder.CreateCall(endFunc, {name});
// 插入end_func(struction)
endCI->insertBefore(IST);
errs() << "function-call-time: " << BB.getParent()->getName()
<< " end\n";
}
}
}
};
} // namespace
char FunctionCallTimePass::ID = 0;
StringRef FunctionCallTimePass::InsertFuncNamePrefix = "_kk_APT";
StringRef FunctionCallTimePass::BeginFuncName = "_kk_APTBeginSection";
StringRef FunctionCallTimePass::EndFuncName = "_kk_APTEndSection";
// 註冊給 opt
// opt -load LLVMFunctionCallTime.dylib -function-call-time xx.bc
static RegisterPass<FunctionCallTimePass>
X("function-call-time", "Function calls take time to collect", false,
false);
// 註冊給 clang 通過 -Xclang -load -Xclang LLVMFunctionCallTime.dylib
static RegisterStandardPasses Y(PassManagerBuilder::EP_EarlyAsPossible,
[](const PassManagerBuilder &Builder,
legacy::PassManagerBase &PM) {
PM.add(new FunctionCallTimePass());
});
chmod +x ./build.sh # 只需要執行一次
./build.sh -b -x
執行成功後,會在LLVM目錄下生成build_dir目錄
build_dir
└── build_xcode
├── CMakeCache.txt
├── CMakeFiles
├── CMakeScripts
├── CPackConfig.cmake
├── CPackSourceConfig.cmake
├── Debug
├── LLVM.xcodeproj
├── LLVMBuild.cmake
├── MinSizeRel
├── RelWithDebInfo
├── Release
├── benchmarks
├── build
├── cmake
├── cmake_install.cmake
├── docs
├── examples
├── include
├── lib
├── llvm.spec
├── projects
├── runtimes
├── test
├── tools
├── unittests
└── utils
打開 LLVM.xcodeproj, 會提示創建scheme, 可以選擇自動,也可以選擇手動, Pass scheme 為LLVMFunctionCallTime
FunctionCallTime Pass target 在 Loadable modules 下面
選擇LLVMFunctionCallTime command + b 進行編譯
默認是Debug模式,所以LLVMFunctionCallTime.dylib產物在build_dir/build_xcode/Debug/lib/LLVMFunctionCallTime.dylib
使用 Pass由於Xcode內置的clang,不支持加載自定義插件,所以直接從LLVM Project Github 倉庫下載編譯好的
由於開始下載的源碼為LLVM 11.0.0, 所以為了一致也是下載相同的版本
wget https://github.com/llvm/llvm-project/releases/download/llvmorg-11.0.0/clang+llvm-11.0.0-x86_64-apple-darwin.tar.xz
mkdir -p ./clang-11.0.0
tar xf clang+llvm-11.0.0-x86_64-apple-darwin.tar.xz -C ./clang-11.0.0 --strip-components 1
下載 everettjf/AppleTrace
將 appletrace.h appletrace.mm 拖入你的工程中
將APTBeginSection APTEndSection APTSyncWait 修改為 kkAPTBeginSection kkAPTEndSection kkAPTSyncWait,為了便於區分在Pass裡面指定了 kkAPT前綴
將appletrace.mm 中WriteSection 改為如下
void WriteSection(const char *name, const char *ph) {
pthread_t thread = pthread_self();
__uint64_t thread_id = 0;
pthread_threadid_np(thread, &thread_id);
uint64_t time = mach_absolute_time() * timeinfo_.numer / timeinfo_.denom;
uint64_t elapsed = (time - begin_) / 1000.0;
if (main_thread_id == 0 && pthread_main_np() != 0) {
main_thread_id = thread_id;
}
if (main_thread_id == thread_id) {
thread_id = 0; // just make main thread id zero
}
// 通過 llvm pass 插樁傳遞過來的 name 最前有個 \0x1 所以需要特殊處理下
// 當然也可以直接在 pass 中處理好
NSString *str = nil;
if (name[0] == '\x01') {
str = [NSString stringWithFormat:@"{\"name\":\"%s\",\"cat\":\"catname\",\"ph\":\"%s\",\"pid\":666,\"tid\":%llu,\"ts\":%llu}",
name + 1, ph, thread_id, elapsed];
} else {
str = [NSString stringWithFormat:@"{\"name\":\"%s\",\"cat\":\"catname\",\"ph\":\"%s\",\"pid\":666,\"tid\":%llu,\"ts\":%llu}",
name, ph, thread_id, elapsed];
}
dispatch_async(queue_, ^{
log_.AddLine(str.UTF8String);
});
}
在你的工程中創建一個xcconfig 文件,內容如下
//
// Config.xcconfig
// PageLinkDemo
//
// Created by king on 2020/12/18.
//
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
LLVM_DIR = $HOME/Documents/LLVM # 修改為在你本機的實際路徑
PASS_DYLIB = $(LLVM_DIR)/build_dir/build_xcode/Debug/lib/LLVMFunctionCallTime.dylib
OTHER_CFLAGS = $(inherited) -Xclang -load -Xclang $(PASS_DYLIB)
OTHER_CPLUSPLUSFLAGS = $(inherited) -Xclang -load -Xclang $(PASS_DYLIB)
COMPILER_INDEX_STORE_ENABLE = NO
CC = $(LLVM_DIR)/clang-11.0.0/bin/clang
CXX = $(LLVM_DIR)clang-11.0.0/bin/clang++
2020-12-20 14:49:32.530027+0800 qmuidemo[25684:1347559] log path = /Users/king/Library/Developer/CoreSimulator/Devices/71211D11-6169-4508-BBD8-B38958350C8F/data/Containers/Data/Application/46E933FE-D962-4355-9E0D-F8CBA44ABFA7/Library/appletracedata/trace.appletrace
通過上面輸出的log path,將appletracedata目錄拷貝到 下載的everettjf/AppleTrace中
將下載的everettjf/AppleTrace merge.py 中 run函數的while循環外面添加如下代碼
self.output.seek(-2, os.SEEK_END)
self.output.truncate()
self.output.write('\n]')
python merge.py -d ./appletracedata
sh get_catapult.sh
# 生成
python catapult/tracing/bin/trace2html appletracedata/trace.json --output=appletracedata/trace.html
# 打開預覽
open trace.html
需要通過opt調試
首先需要先編譯好Pass
生成IR文件
clang -x objective-c++ -isysroot $(xcrun --sdk macosx --show-sdk-path) -S -emit-llvm -c xxx.m -o xx.ll
切換為opt scheme, 選擇Edit Scheme 設置 opt的啟動參數,如下
然後運行 opt, 由於opt依賴很多,所有需要編譯很長時間,不過如果你是40萬的Mac Pro,那麼可能只需要幾秒或者十幾秒
這樣就可以直接在Pass源碼中下斷點
LLVMFunctionCallTimePass 源碼
參考Writing an LLVM Pass
Clang 插件統計方法耗時
就差您點一下了 👇👇👇