目次: GCC
目次: Linux
AArch64(64ビットARM)向けのLinux開発環境を構築しました。超有名ツールの組み合わせだし、簡単だろうと思っていたのですが、意外とハマって1日掛かってしまったのでメモしておきます。使用するツールは下記の通りです。
クロスコンパイラを生成するためcrosstool-NG(GitHubへのリンク)を使います。crosstool-NGはARM向け以外にもクロスコンパイラやツールチェインの構築が簡単にできて便利です。
私の環境(Debian Testing)だとlibtool-binパッケージがインストールされていなくてハマったので、インストールしておくと良いかもしれません。
$ git clone https://github.com/crosstool-ng/crosstool-ng $ cd crosstool-ng $ ./bootstrap INFO :: *** Generating package version descriptions INFO :: Master packages: android-ndk autoconf automake avr-libc binutils cloog duma elf2flt expat gcc gdb gettext glibc-ports glibc gmp isl libelf libiconv libtool linux ltrace m4 make mingw-w64 mpc mpfr musl ncurses newlib strace uClibc zlib INFO :: Generating 'config/versions/android-ndk.in' INFO :: Generating 'config/versions/autoconf.in' ...(snip)... INFO :: Generating comp_libs.in (menu) INFO :: *** Gathering the list of data files to install INFO :: *** Running autoreconf INFO :: *** Done! $ ./configure --enable-local checking for a BSD-compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking for a thread-safe mkdir -p... /bin/mkdir -p ...(snip)... config.status: creating config.h config.status: config.h is unchanged config.status: executing depfiles commands $ make /usr/bin/make all-recursive make[1]: Entering directory '/home/katsuhiro/share/projects/oss/crosstool-ng' Making all in kconfig make[2]: Entering directory '/home/katsuhiro/share/projects/oss/crosstool-ng/kconfig' bison -y -l -b zconf -p zconf -ozconf.c zconf.y ...(snip)... /bin/mkdir -p docs && ( /bin/sed -e 's,[@]docdir[@],/usr/local/share/doc/crosstool-ng,g' -e 's,[@]pkgdatadir[@],/usr/local/share/crosstool-ng,g' -e 's,[@]pkglibexecdir[@],/usr/local/libexec/crosstool-ng,g' -e 's,[@]progname[@],'`echo ct-ng | sed 's,x,x,'`',g' | /bin/bash config.status --file=- ) < docs/ct-ng.1.in >docs/ct-ng.1-t && mv -f docs/ct-ng.1-t docs/ct-ng.1 make[2]: Leaving directory '/home/katsuhiro/share/projects/oss/crosstool-ng' make[1]: Leaving directory '/home/katsuhiro/share/projects/oss/crosstool-ng'
カレントディレクトリにct-ngという名前のファイルが生成されます。通常はクロスコンパイラをインストールして使いますが、私はインストールしないで欲しい(適宜入れ替えたいから)ので、--enable-localオプションを付けています。
$ ./ct-ng menuconfig
- Target options --->
Target Architecture (alpha) --->
armに変更する
Bitness: (32-bit) --->
64-bitに変更する
- Operating System --->
Target OS (bare-metal) --->
linuxに変更する
- C compiler --->
C++ (NEW) を選択する
$ ./ct-ng build
[00:34] /
ビルド中は多少メッセージも出ますが、基本的に経過時間と棒がくるくる回るだけです。ログが見たい方は、ct-ngと同じディレクトリにあるbuild.logをtail -fなどで表示すると良いでしょう。
マシン性能によりますが、./ct-ng buildによるクロスコンパイラのビルドは1時間くらい掛かると思います。ビルドが終わると、ホームディレクトリにx-toolsというディレクトリが作られていると思います。AArch64用のクロスコンパイラ(gcc 8)をビルドした場合、x-tools以下は下記のようになっているはずです。
$ cd ~/x-tools $ ls aarch64-unknown-linux-gnu $ ls aarch64-unknown-linux-gnu aarch64-unknown-linux-gnu bin build.log.bz2 include lib libexec share $ ls aarch64-unknown-linux-gnu/bin aarch64-unknown-linux-gnu-addr2line aarch64-unknown-linux-gnu-gcov-dump aarch64-unknown-linux-gnu-ar aarch64-unknown-linux-gnu-gcov-tool aarch64-unknown-linux-gnu-as aarch64-unknown-linux-gnu-gfortran aarch64-unknown-linux-gnu-c++ aarch64-unknown-linux-gnu-gprof aarch64-unknown-linux-gnu-c++filt aarch64-unknown-linux-gnu-ld aarch64-unknown-linux-gnu-cc aarch64-unknown-linux-gnu-ld.bfd aarch64-unknown-linux-gnu-cpp aarch64-unknown-linux-gnu-ld.gold aarch64-unknown-linux-gnu-ct-ng.config aarch64-unknown-linux-gnu-ldd aarch64-unknown-linux-gnu-dwp aarch64-unknown-linux-gnu-nm aarch64-unknown-linux-gnu-elfedit aarch64-unknown-linux-gnu-objcopy aarch64-unknown-linux-gnu-g++ aarch64-unknown-linux-gnu-objdump aarch64-unknown-linux-gnu-gcc aarch64-unknown-linux-gnu-populate aarch64-unknown-linux-gnu-gcc-7.3.0 aarch64-unknown-linux-gnu-ranlib aarch64-unknown-linux-gnu-gcc-ar aarch64-unknown-linux-gnu-readelf aarch64-unknown-linux-gnu-gcc-nm aarch64-unknown-linux-gnu-size aarch64-unknown-linux-gnu-gcc-ranlib aarch64-unknown-linux-gnu-strings aarch64-unknown-linux-gnu-gcov aarch64-unknown-linux-gnu-strip
クロスコンパイラは~/x-tools/aarch64-unknown-linux-gnu/bin以下にあります。今後、このクロスコンパイラを使います。
カーネルはLinuxの開発版linux-nextを使います(Gitリポジトリへのリンク)。StableカーネルやLTSカーネルも同じ手順で良いと思いますが、古いカーネルをビルドするときは、crosstool-NGでgcc 7かgcc 6を選択したほうが良いかもしれません(ビルド時に警告が出てくると邪魔なので…)。
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git $ cd linux-next $ export ARCH=arm64 $ export CROSS_COMPILE=~/x-tools/aarch64-unknown-linux-gnu/bin/aarch64-unknown-linux-gnu- $ make defconfig $ make all scripts/kconfig/conf --syncconfig Kconfig WRAP arch/arm64/include/generated/uapi/asm/ioctl.h WRAP arch/arm64/include/generated/uapi/asm/errno.h WRAP arch/arm64/include/generated/uapi/asm/ioctls.h ...(snip)... LD [M] sound/soc/generic/snd-soc-simple-card-utils.ko LD [M] sound/soc/generic/snd-soc-simple-card.ko LD [M] sound/soc/sh/rcar/snd-soc-rcar.ko
マシン性能によりますが、カーネルのビルドは数十分くらい掛かると思います。AArch64用のカーネルをビルドした場合、arch/arm64/boot以下は下記のようになっているはずです。
$ cd arch/arm64/boot $ ls Image Image.gz Makefile dts install.sh
ビルドが終わると、arch/arm64/boot以下にImageという名前のファイルが作られていると思います。このイメージファイルを後で使います。
この記事にコメントする
もはや自分以外の誰得の内容なのか、わかりませんが、気にせず書きます。
その3にて「DescramblerImplを生成するのはMediaCasService::createDescrambler() のみ?のように見えます。」と書きました。この関数に至る経路がわかると、デスクランブラがどの暗号系を選択するのか?いつ選択するのか?などがわかるようになります。
で、今回はcreateDescrambler() が呼ばれる経路を見つけたのでメモします。しかしながらExtractorのsetMediaCas() を呼び出すのは誰なのか?まだ謎のままなので、いまいちスッキリしませんけども。
//frameworks/av/media/libstagefright/mpeg2ts/MPEG2TSExtractor.cpp
status_t MPEG2TSExtractor::setMediaCas(const HInterfaceToken &casToken) {
HalToken halToken;
halToken.setToExternal((uint8_t*)casToken.data(), casToken.size());
sp<ICas> cas = ICas::castFrom(retrieveHalInterface(halToken));
ALOGD("setMediaCas: %p", cas.get());
status_t err = mParser->setMediaCas(cas); //★★★★mParser
if (err == OK) {
ALOGI("All tracks now have descramblers");
init();
}
return err;
}
ここで出てくるmParserの型はATSParserでしたので、ATSParserの実装を見てみます。
//frameworks/av/media/libstagefright/mpeg2ts/ATSParser.cpp
status_t ATSParser::setMediaCas(const sp<ICas> &cas) {
status_t err = mCasManager->setMediaCas(cas); //★★★★mCasManager
if (err != OK) {
return err;
}
for (size_t i = 0; i < mPrograms.size(); ++i) {
mPrograms.editItemAt(i)->updateCasSessions();
}
return OK;
}
ここで出てくるmCasManagerの型はATSParser::CasManagerでした。CasManagerを見てみましょう。
//frameworks/av/media/libstagefright/mpeg2ts/CasManager.cpp
status_t ATSParser::CasManager::setMediaCas(const sp<ICas> &cas) {
if (cas == NULL) {
ALOGE("setMediaCas: received NULL object");
return BAD_VALUE;
}
if (mICas != NULL) {
ALOGW("setMediaCas: already set");
return ALREADY_EXISTS;
}
for (size_t index = 0; index < mProgramCasMap.size(); index++) {
status_t err;
if ((err = mProgramCasMap.editValueAt(
index)->setMediaCas(cas, mCAPidToSessionIdMap)) != OK) { //★★★★mProgramCasMap
return err;
}
}
mICas = cas; //★★★★mICas
return OK;
}
Extractorから渡されてきたcasは、CasManagerのメンバ変数mICasに保存されるようです。ちなみにこのmICasはその2のCasManager::parsePID() にて、mICas->processEcm(mCAPidToSessionIdMap[index], ecm); の呼び出し時に出てきました。ここで保存されていたんですね。
話を戻してmProgramCasMapはunsignedがキー、ProgramCasManagerのポインタが値のKeyedVectorです。
status_t ATSParser::CasManager::ProgramCasManager::setMediaCas(
const sp<ICas> &cas, PidToSessionMap &sessionMap) {
if (mHasProgramCas) {
return initSession(cas, sessionMap, &mProgramCas); //★★★★
}
// TODO: share session among streams that has identical CA_descriptors.
// For now, we open one session for each stream that has CA_descriptor.
for (size_t index = 0; index < mStreamPidToCasMap.size(); index++) {
status_t err = initSession(
cas, sessionMap, &mStreamPidToCasMap.editValueAt(index)); //★★★★
if (err != OK) {
return err;
}
}
return OK;
}
status_t ATSParser::CasManager::ProgramCasManager::initSession(
const sp<ICas>& cas,
PidToSessionMap &sessionMap,
CasSession *session) {
sp<IMediaCasService> casService = IMediaCasService::getService("default"); //★★★★IMediaCasService型
if (casService == NULL) {
ALOGE("Cannot obtain IMediaCasService");
return NO_INIT;
}
//...
returnDescrambler = casService->createDescrambler(descriptor.mSystemID); //★★★★createDescrambler()
if (!returnDescrambler.isOk()) {
ALOGE("Failed to create descrambler: trans=%s",
returnDescrambler.description().c_str());
goto l_fail;
}
descramblerBase = (sp<IDescramblerBase>) returnDescrambler;
if (descramblerBase == NULL) {
ALOGE("Failed to create descrambler: null ptr");
goto l_fail;
}
やっとcreateDescrambler() が出てきました。casServiceはIMediaCasService型ですが、このインタフェースを実装しているクラスは1つしかなさそうです。
//hardware/interfaces/cas/1.0/default/MediaCasService.h
class MediaCasService : public IMediaCasService {
//...
//hardware/interfaces/cas/1.0/default/MediaCasService.cpp
Return<sp<IDescramblerBase>> MediaCasService::createDescrambler(int32_t CA_system_id) {
ALOGV("%s: CA_system_id=%d", __FUNCTION__, CA_system_id);
sp<IDescrambler> result;
DescramblerFactory *factory;
sp<SharedLibrary> library;
if (mDescramblerLoader.findFactoryForScheme(
CA_system_id, &library, &factory)) { //★★★★DescramblerPluginを探す処理はこの辺にありそう
DescramblerPlugin *plugin = NULL;
if (factory->createPlugin(CA_system_id, &plugin) == OK
&& plugin != NULL) {
result = new DescramblerImpl(library, plugin); //★★★★DescramblerImplを生成している箇所があった
}
}
return result;
}
ここでゴールのようです。まとめるとExtractorのsetMediaCas() を呼ぶと、
この記事にコメントする
デスクランブル処理はどこにあるんでしょう??それらしい処理を辿ってみましたが、本当にこの処理が動くのか、何を切っ掛けにデスクランブル処理が働き始めるのか、動かしてみないとわからなさそうです。うーん……。
//frameworks/base/media/java/android/media/MediaDescrambler.java
public final int descramble(
@NonNull ByteBuffer srcBuf, @NonNull ByteBuffer dstBuf,
@NonNull MediaCodec.CryptoInfo cryptoInfo) {
//...
try {
return native_descramble(
cryptoInfo.key[0],
cryptoInfo.numSubSamples,
cryptoInfo.numBytesOfClearData,
cryptoInfo.numBytesOfEncryptedData,
srcBuf, srcBuf.position(), srcBuf.limit(),
dstBuf, dstBuf.position(), dstBuf.limit());
} catch (ServiceSpecificException e) {
MediaCasStateException.throwExceptionIfNeeded(e.errorCode, e.getMessage());
} catch (RemoteException e) {
cleanupAndRethrowIllegalState();
}
return -1;
}
ここで呼び出しているnative_descramble() はnativeであると宣言されています。つまりJNI経由で呼び出します。メディアフレームワーク(android.media)のJNI実装はframeworks/base/media/jniに配置されているようです。
//frameworks/base/media/jni/android_media_MediaDescrambler.cpp
static jint android_media_MediaDescrambler_native_descramble(
JNIEnv *env, jobject thiz, jbyte key, jint numSubSamples,
jintArray numBytesOfClearDataObj, jintArray numBytesOfEncryptedDataObj,
jobject srcBuf, jint srcOffset, jint srcLimit,
jobject dstBuf, jint dstOffset, jint dstLimit) {
sp<JDescrambler> descrambler = getDescrambler(env, thiz); //★★★★descramblerはJDescrambler型
if (descrambler == NULL) {
jniThrowException(env, "java/lang/IllegalStateException",
"Invalid descrambler object!");
return -1;
}
hidl_vec<SubSample> subSamples;
ssize_t totalLength = getSubSampleInfo(
env, numSubSamples, numBytesOfClearDataObj,
numBytesOfEncryptedDataObj, &subSamples);
if (totalLength < 0) {
jniThrowException(env, "java/lang/IllegalArgumentException",
"Invalid subsample info!");
return -1;
}
//...
err = descrambler->descramble(
key, totalLength, subSamples,
srcPtr, srcOffset, dstPtr, dstOffset,
&status, &bytesWritten, &detailedError); //★★★★
//...
status_t JDescrambler::descramble(
jbyte key,
ssize_t totalLength,
const hidl_vec<SubSample>& subSamples,
const void *srcPtr,
jint srcOffset,
void *dstPtr,
jint dstOffset,
Status *status,
uint32_t *bytesWritten,
hidl_string *detailedError) {
//...
auto err = mDescrambler->descramble(
(ScramblingControl) key,
subSamples,
mDescramblerSrcBuffer,
0,
dstBuffer,
0,
[&status, &bytesWritten, &detailedError] (
Status _status, uint32_t _bytesWritten,
const hidl_string& _detailedError) {
*status = _status;
*bytesWritten = _bytesWritten;
*detailedError = _detailedError;
}); //★★★★
//...
ここで出てくるmDescramblerはIDescrambler型のポインタです。パッと見では、何がセットされているのか良くわかりませんが、このインタフェースを実装しているのは下記しかなさそうです。
//hardware/interfaces/cas/1.0/default/DescramblerImpl.h
class DescramblerImpl : public IDescrambler {
//...
//hardware/interfaces/cas/1.0/default/DescramblerImpl.cpp
Return<void> DescramblerImpl::descramble(
ScramblingControl scramblingControl,
const hidl_vec<SubSample>& subSamples,
const SharedBuffer& srcBuffer,
uint64_t srcOffset,
const DestinationBuffer& dstBuffer,
uint64_t dstOffset,
descramble_cb _hidl_cb) {
ALOGV("%s", __FUNCTION__);
// Get a local copy of the shared_ptr for the plugin. Note that before
// calling the HIDL callback, this shared_ptr must be manually reset,
// since the client side could proceed as soon as the callback is called
// without waiting for this method to go out of scope.
std::shared_ptr<DescramblerPlugin> holder = std::atomic_load(&mPluginHolder); //★★★★holder = mPluginHolder
if (holder.get() == nullptr) {
_hidl_cb(toStatus(INVALID_OPERATION), 0, NULL);
return Void();
}
//...
// Casting hidl SubSample to DescramblerPlugin::SubSample, but need
// to ensure structs are actually idential
int32_t result = holder->descramble(
dstBuffer.type != BufferType::SHARED_MEMORY,
(DescramblerPlugin::ScramblingControl)scramblingControl,
subSamples.size(),
(DescramblerPlugin::SubSample*)subSamples.data(),
srcPtr,
srcOffset,
dstPtr,
dstOffset,
NULL);
//...
このmPluginHolderはDescramblerPlugin型です。DescramblerImplが生成される時に設定されます。DescramblerImplを生成するのはMediaCasService::createDescrambler() のみ?のように見えます。
次に出てくるholderにはDescramblerPluginのポインタが入ります。Pluginの実装を探してみるとClearKeyにそれらしき処理があります。
//frameworks/av/drm/mediacas/plugins/clearkey/ClearKeyCasPlugin.cpp
ssize_t ClearKeyDescramblerPlugin::descramble(
bool secure,
ScramblingControl scramblingControl,
size_t numSubSamples,
const SubSample *subSamples,
const void *srcPtr,
int32_t srcOffset,
void *dstPtr,
int32_t dstOffset,
AString *errorDetailMsg) {
ALOGV("descramble: secure=%d, sctrl=%d, subSamples=%s, "
"srcPtr=%p, dstPtr=%p, srcOffset=%d, dstOffset=%d",
(int)secure, (int)scramblingControl,
subSamplesToString(subSamples, numSubSamples).string(),
srcPtr, dstPtr, srcOffset, dstOffset);
if (mCASSession == NULL) {
ALOGE("Uninitialized CAS session!");
return ERROR_CAS_DECRYPT_UNIT_NOT_INITIALIZED;
}
return mCASSession->decrypt(
secure, scramblingControl,
numSubSamples, subSamples,
(uint8_t*)srcPtr + srcOffset,
dstPtr == NULL ? NULL : ((uint8_t*)dstPtr + dstOffset),
errorDetailMsg);
}
ここで出てくるmCASSessionはClearKeyCasSessionを指すようです。setMediaCasSession() にて設定されています。これは後で追ってみます。
//av/drm/mediacas/plugins/clearkey/ClearKeyCasPlugin.cpp
// Decryption of a set of sub-samples
ssize_t ClearKeyCasSession::decrypt(
bool secure, DescramblerPlugin::ScramblingControl scramblingControl,
size_t numSubSamples, const DescramblerPlugin::SubSample *subSamples,
const void *srcPtr, void *dstPtr, AString * /* errorDetailMsg */) {
return ERROR_CAS_CANNOT_HANDLE;
AES_KEY contentKey;
// Hold lock to get the key only to avoid contention for decryption
Mutex::Autolock _lock(mKeyLock);
int32_t keyIndex = (scramblingControl & 1);
ALOGE("decrypt: key %d is invalid", keyIndex);
return ERROR_CAS_DECRYPT;
contentKey = mKeyInfo[keyIndex].contentKey; //★★★★ClearKeyCasSession::updateECM() で設定する鍵だと思う
//...
// Don't decrypt if len < AES_BLOCK_SIZE.
// The last chunk shorter than AES_BLOCK_SIZE is not encrypted.
if (scramblingControl != DescramblerPlugin::kScrambling_Unscrambled
&& subSamples[i].mNumBytesOfEncryptedData >= AES_BLOCK_SIZE) {
err = decryptPayload(
contentKey,
numBytesinSubSample,
subSamples[i].mNumBytesOfClearData,
(char *)dst);
}
//...
// Decryption of a TS payload
status_t ClearKeyCasSession::decryptPayload(
const AES_KEY& key, size_t length, size_t offset, char* buffer) const {
CHECK(buffer);
// Invariant: only call decryptPayload with TS packets with at least 16
// bytes of payload (AES_BLOCK_SIZE).
CHECK(length >= offset + AES_BLOCK_SIZE);
return TpBlockCtsDecrypt(key, length - offset, buffer + offset);
}
// AES-128 CBC-CTS decrypt optimized for Transport Packets. |key| is the AES
// key (odd key or even key), |length| is the data size, and |buffer| is the
// ciphertext to be decrypted in place.
status_t TpBlockCtsDecrypt(const AES_KEY& key, size_t length, char* buffer) {
CHECK(buffer);
//...
CBCモードでAES暗号を復号しているようです。
まとめると、MediaDescrambler(ドキュメントはここ)のdescrambler() によって、スクランブルされたデータのデスクランブルができそう、ということがわかりました。
ドキュメントにそう書いてあるし、当たり前なんですけどね。新たにデスクランブラを足そうと思う人は、中身も知らないと足せないです。
この記事にコメントする
昨日の続き。スクランブルの掛かったストリームはmParser->mCasManagerに任せていました。mCasManagerはATSParser::CasManagerでしたので、実装を見てみます。
//frameworks/av/media/libstagefright/mpeg2ts/CasManager.cpp
bool ATSParser::CasManager::parsePID(ABitReader *br, unsigned pid) {
ssize_t index = mCAPidToSessionIdMap.indexOfKey(pid);
if (index < 0) {
return false;
}
hidl_vec<uint8_t> ecm;
ecm.setToExternal((uint8_t*)br->data(), br->numBitsLeft() / 8);
auto returnStatus = mICas->processEcm(mCAPidToSessionIdMap[index], ecm); //★★★★processEcm()
if (!returnStatus.isOk() || (Status) returnStatus != Status::OK) {
ALOGE("Failed to process ECM: trans=%s, status=%d",
returnStatus.description().c_str(), (Status) returnStatus);
}
return true; // handled
}
謎のmICasがどこから来るかは、後で調べるとして、関数名processEcm() で探してみると、HALの方にコードがあります。
//hardware/interfaces/cas/1.0/default/CasImpl.cpp
Return<Status> CasImpl::processEcm(
const HidlCasSessionId &sessionId, const HidlCasData& ecm) {
ALOGV("%s: sessionId=%s", __FUNCTION__,
sessionIdToString(sessionId).string());
std::shared_ptr<CasPlugin> holder = std::atomic_load(&mPluginHolder); //★★★★CasPlugin
if (holder.get() == nullptr) {
return toStatus(INVALID_OPERATION);
}
return toStatus(holder->processEcm(sessionId, ecm));
}
想像するにCasPluginというクラスを派生させて処理を実装するのでしょう。探してみるとframeworks/av/drm/mediacas/plugins以下にclearkeyとmockという実装があります。
//frameworks/av/drm/mediacas/plugins/clearkey/ClearKeyCasPlugin.h
class ClearKeyCasPlugin : public CasPlugin {
...
//frameworks/av/drm/mediacas/plugins/clearkey/ClearKeyCasPlugin.cpp
status_t ClearKeyCasPlugin::processEcm(
const CasSessionId &sessionId, const CasEcm& ecm) {
ALOGV("processEcm: sessionId=%s", sessionIdToString(sessionId).string());
sp<ClearKeyCasSession> session =
ClearKeySessionLibrary::get()->findSession(sessionId);
if (session == NULL) {
return ERROR_CAS_SESSION_NOT_OPENED;
}
Mutex::Autolock lock(mKeyFetcherLock);
return session->updateECM(mKeyFetcher.get(), (void*)ecm.data(), ecm.size()); //★★★★mKeyFetcher
}
status_t ClearKeyCasSession::updateECM(
KeyFetcher *keyFetcher, void *ecm, size_t size) {
//...
uint64_t asset_id;
std::vector<KeyFetcher::KeyInfo> keys;
status_t err = keyFetcher->ObtainKey(mEcmBuffer, &asset_id, &keys); //★★★★keyFetcher
if (err != OK) {
ALOGE("updateECM: failed to obtain key (err=%d)", err);
return err;
}
ALOGV("updateECM: %zu key(s) found", keys.size());
for (size_t keyIndex = 0; keyIndex < keys.size(); keyIndex++) {
String8 str;
const sp<ABuffer>& keyBytes = keys[keyIndex].key_bytes;
CHECK(keyBytes->size() == kUserKeyLength);
int result = AES_set_decrypt_key(
reinterpret_cast<const uint8_t*>(keyBytes->data()),
AES_BLOCK_SIZE * 8, &mKeyInfo[keyIndex].contentKey); //★★★★libsslの関数に渡して鍵を生成している?ようだ
//...
//frameworks/av/drm/mediacas/plugins/clearkey/ClearKeyFetcher.cpp
status_t ClearKeyFetcher::ObtainKey(const sp<ABuffer>& buffer,
uint64_t* asset_id, std::vector<KeyInfo>* keys) {
//...
引数に渡しているmKeyFetcher(とget() が返すkeyFetcherも同様に)はKeyFetcher型のポインタでした。KeyFetcherを継承したClearKeyFetcher型のオブジェクトが格納されていました。
ClearKeyの仕組みは詳しく知りませんが、ClearKeyCasSession::updateECM() でAESの復号などをしていることと、AESの復号鍵はClearKeyFetcher::ObtainKey() がECMを読んで復号鍵を取得してくれるように見えました。
AndroidでECMの解読を行っている箇所が見つけられました。エレメンタリストリームのデスクランブルはどこで行っているのでしょうね…??
この記事にコメントする
Android 8がMPEG2-TSのPSI(Program Specific Information)をどのように処理しているのか、気になったので調べてみました。調査に使ったコードはAOSPのタグandroid-8.1.0_r33です。
PSIのことをセクションと呼ぶ人もいますね。MPEG2 Systemの規格書ISO13818-1/ITU-T H.222.0によれば、PSIはxxx Tableという名前(PATならProgram Association Table)で、テーブルは1つないし、複数のセクション(xxx_sectionという名前で定義されている、PATならprogram_association_section)から構成されるからだと思います。
さておきTSを処理しているところは、下記のようになっています。
//frameworks/av/media/libstagefright/mpeg2ts/MPEG2TSExtractor.cpp
status_t MPEG2TSExtractor::feedMore(bool isInit) {
Mutex::Autolock autoLock(mLock);
uint8_t packet[kTSPacketSize];
ssize_t n = mDataSource->readAt(mOffset, packet, kTSPacketSize);
if (n < (ssize_t)kTSPacketSize) {
if (n >= 0) {
mParser->signalEOS(ERROR_END_OF_STREAM);
}
return (n < 0) ? (status_t)n : ERROR_END_OF_STREAM;
}
ATSParser::SyncEvent event(mOffset);
mOffset += n;
status_t err = mParser->feedTSPacket(packet, kTSPacketSize, &event); //★★★★
if (event.hasReturnedData()) {
if (isInit) {
mLastSyncEvent = event;
} else {
addSyncPoint_l(event);
}
}
return err;
}
ここで出てくるmParserはATSParserのポインタなので、ATSParserの実装を見てみます。
//frameworks/av/media/libstagefright/mpeg2ts/ATSParser.cpp
status_t ATSParser::feedTSPacket(const void *data, size_t size,
SyncEvent *event) {
if (size != kTSPacketSize) {
ALOGE("Wrong TS packet size");
return BAD_VALUE;
}
ABitReader br((const uint8_t *)data, kTSPacketSize);
return parseTS(&br, event); //★★★★
}
status_t ATSParser::parseTS(ABitReader *br, SyncEvent *event) {
ALOGV("---");
//...
status_t err = OK;
unsigned random_access_indicator = 0;
if (adaptation_field_control == 2 || adaptation_field_control == 3) {
err = parseAdaptationField(br, PID, &random_access_indicator);
}
if (err == OK) {
if (adaptation_field_control == 1 || adaptation_field_control == 3) {
err = parsePID(br, PID, continuity_counter,
payload_unit_start_indicator,
transport_scrambling_control,
random_access_indicator,
event); //★★★★
}
}
//...
status_t ATSParser::parsePID(
ABitReader *br, unsigned PID,
unsigned continuity_counter,
unsigned payload_unit_start_indicator,
unsigned transport_scrambling_control,
unsigned random_access_indicator,
SyncEvent *event) {
ssize_t sectionIndex = mPSISections.indexOfKey(PID);
//...
if (sectionIndex >= 0) { //★★★★PATかPMTのPIDならこの条件が成り立つ
sp<PSISection> section = mPSISections.valueAt(sectionIndex);
ここで出てくるmPSISectionはunsignedをキー、sp<PSISection> を値とするKeyedVectorです。キー0にPATを持っていて、それ以外のキーはPMTのPID(PATが一覧を持っている)です。PMTのPIDはPATを受信したときにATSParser::parseProgramAssociationTable() が追加するようです。
//frameworks/av/media/libstagefright/mpeg2ts/ATSParser.cpp
status_t ATSParser::parsePID(
ABitReader *br, unsigned PID,
unsigned continuity_counter,
unsigned payload_unit_start_indicator,
unsigned transport_scrambling_control,
unsigned random_access_indicator,
SyncEvent *event) {
ssize_t sectionIndex = mPSISections.indexOfKey(PID);
//...
if (sectionIndex >= 0) { //★★★★PATかPMTのPIDならこの条件が成り立つ
sp<PSISection> section = mPSISections.valueAt(sectionIndex);
//...
if (PID == 0) {
parseProgramAssociationTable(§ionBits); //★★★★PID 0ならPATの解析
} else {
bool handled = false;
for (size_t i = 0; i < mPrograms.size(); ++i) { //★★★★ それ以外はPMTかどうか見る
status_t err;
if (!mPrograms.editItemAt(i)->parsePSISection( //★★★★PMTか?
PID, §ionBits, &err)) {
continue;
}
//...
bool ATSParser::Program::parsePSISection(
unsigned pid, ABitReader *br, status_t *err) {
*err = OK;
if (pid != mProgramMapPID) {
return false;
}
*err = parseProgramMap(br); //★★★★PMTだったのでPMTの解析
return true;
}
status_t ATSParser::Program::parseProgramMap(ABitReader *br) {
unsigned table_id = br->getBits(8);
ALOGV(" table_id = %u", table_id);
//...
// descriptors
CADescriptor programCA;
bool hasProgramCA = findCADescriptor(br, program_info_length, &programCA); //★★★★PMTの持っているdescriptorを見ている
if (hasProgramCA && !mParser->mCasManager->addProgram(
mProgramNumber, programCA)) { //★★★★CA descriptorの指すPIDつまりECMのPIDを追加
return ERROR_MALFORMED;
}
//...
size_t infoBytesRemaining = section_length - 9 - program_info_length - 4;
while (infoBytesRemaining >= 5) { //★★★★ エレメンタリストリームのPIDと一緒に付いているdescriptorを見ている
//...
CADescriptor streamCA;
bool hasStreamCA = findCADescriptor(br, ES_info_length, &streamCA);
if (hasStreamCA && !mParser->mCasManager->addStream(
mProgramNumber, elementaryPID, streamCA)) { //★★★★CA descriptorの指すPIDつまりECMのPIDを追加
return ERROR_MALFORMED;
}
//...
}
//...
for (size_t i = 0; i < infos.size(); ++i) {
StreamInfo &info = infos.editItemAt(i);
if (mParser->mCasManager->isCAPid(info.mPID)) { //★★★★CA descriptorに記載のあったストリーム
// skip CA streams (EMM/ECM)
continue;
}
ssize_t index = mStreams.indexOfKey(info.mPID);
if (index < 0) {
sp<Stream> stream = new Stream(
this, info.mPID, info.mType, PCR_PID, info.mCASystemId);
if (mSampleAesKeyItem != NULL) {
stream->signalNewSampleAesKey(mSampleAesKeyItem);
}
isAddingScrambledStream |= info.mCASystemId >= 0; //★★★★CA descriptorに記載が無いのにスクランブルされている??
mStreams.add(info.mPID, stream);
}
}
ざっくり言うと、スクランブルの掛かったストリームはmParser->mCasManagerに任せ、スクランブルの掛かっていないストリームはmStreamsに任せるようです。
CA descriptorに載っていないのにスクランブルの掛かった変なストリームがあると警告が出るようになっています。
この記事にコメントする
先日、作った(2018年6月1日の日記参照)ISDBというかARIBのデスクランブラの続きです。
DVB APIで制御可能なチューナー(私はPT2で確認しています)を使っている方であれば、下記のようにチューニング(コードは GitHubにあります)できます。チューニングに成功して放送が受信されると、/dev/dvb/adapter0/dvr0からスクランブルの掛かったMPEG2-TSが出力されます。
例: BSプレミアム(衛星はアダプタ0か2、地デジはアダプタ1か3を使います) $ ./sample_dvb 0 S BS 3 0x4031 ... ごちゃごちゃ出るのが邪魔くさければ、 $ ./sample_dvb 0 S BS 3 0x4031 > /dev/null
スマートカードリーダーをPCに接続し、B-CASカードをリーダーに挿入した上で、下記のようにデスクランブル(コードは GitHubにあります)できます。デスクランブルしたMPEG2-TSはUDPで送るか、ファイルに保存できます。
例: 自分自身にUDPで送る $ ./arib_descramble /dev/dvb/adapter0/dvr0 localhost 1234 ...
VLCを起動し、udp://@:1234を再生すると、受信中の放送が映るはずです。
自身の規格理解のためもあって、かなり手抜き実装していて、異常に重いため、いくつか改良してみました。まずプロファイラで見てみると、MULTI2復号と、どこかにある無駄なコピーに、時間がかかっているようです。
MULTI2復号の高速化にはSSE2を使ってみました。MULTI2の復号は8バイトずつですが、SSE2を活用するには32バイトの方が都合が良いです。ですので4単位まとめて(4 x 8バイト = 32バイト = 128bit = SSE2のレジスタ幅)処理して、残った32バイトに満たないデータは従来どおり8バイトずつ処理します。
残念ながら、結果から言うとあまり最適化が効きませんでした。SIMDで高速化できないロード/ストアの割合が高いのか、計算が占める割合が低いのか、いまいちわからなかったのですが、あまり高速化できませんでした。CPU利用率でいうと12% が11% になるか、ならないか…程度です。
無駄なコピーは2箇所見つけたのでガッツリ消しました。これは効果があったようで、CPU利用率でいうと11% が10% くらいまで削減できました。
無駄なコピーはもう1つありましたが、単純に消すわけにいかなくてやや難しそうだったので、また今度にします。
PCだと、CPU利用率10% 程度だったので、最近のマルチコアCPUなら割と余裕の負荷です。ではショボいCPUで実行するとどうなるか、試してみました。
手持ちのRaspberry Pi 3(ARM Cortex A53 x 4/1.4GHz)で実行してみたところ、CPU利用率25〜27% 程度でした。動かないかもしれないと思っていたので、正直意外でした。かなり健闘していると思います。
ARMにはNEONというSIMD命令がありますが、NEONを使った復号の高速化にはまだ手を出していません。今度やってみますが、SSE2の結果を見た限りでは、絶大な効果は見込めないでしょう。きっと。
この記事にコメントする
目次: Linux
Raspberry Pi 3を持っているのですが、あまり速くない(当たり前ですけど)こともあり、ほとんどコンパイルには使っていませんでした。今日、久しぶりにコードのビルドに使ってみたら、変な症状にハマりました。
$ autoconf --version autoconf (GNU Autoconf) 2.69 ... $ automake --version automake (GNU automake) 1.15 ... $ autoreconf -fi aclocal: warning: couldn't open directory 'm4': No such file or directory configure.ac:23: installing 'conf/compile' configure.ac:16: installing 'conf/install-sh' configure.ac:16: installing 'conf/missing' src/Makefile.am: installing 'conf/depcomp'
更地からのビルドなのでautoreconf -fiを実行しています。この時点では特にエラーも出ずに終わったように見えます。
$ ./configure checking for a BSD-compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking for a thread-safe mkdir -p... /bin/mkdir -p checking for gawk... gawk checking whether make sets $(MAKE)... yes checking whether make supports nested variables... yes checking whether make supports nested variables... (cached) yes ./configure: line 2505: syntax error near unexpected token `ac_ext=c' ./configure: line 2505: `ac_ext=c'
しかしconfigureが謎のエラーで終了してしまいます。しかもconfig.logにエラーの内容が記録されておらず、怪しいです。
しばしconfigure.acをいじってみてわかったことは、以下の条件を満たしていると、このエラーが発生するようです。
解決策はlibtoolをインストールするか、libtoolを使っていないならconfigure.acからLT_INIT() を削除しても良いです。
この辺の仕組みは詳しくありませんが、libtoolが無いなら無いと言ってくれれば、もう少しわかりやすいのにな…と思います。
この記事にコメントする
| < | 2018 | > | ||||
| << | < | 07 | > | >> | ||
| 日 | 月 | 火 | 水 | 木 | 金 | 土 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 | - | - | - | - |
25年10月6日
25年10月6日
25年9月29日
25年9月29日
20年8月24日
20年8月24日
16年2月14日
16年2月14日
25年7月20日
25年7月20日
25年7月20日
25年7月20日
25年7月20日
25年7月20日
20年8月16日
20年8月16日
20年8月16日
20年8月16日
24年6月17日
24年6月17日
wiki
Linux JM
Java API
2002年
2003年
2004年
2005年
2006年
2007年
2008年
2009年
2010年
2011年
2012年
2013年
2014年
2015年
2016年
2017年
2018年
2019年
2020年
2021年
2022年
2023年
2024年
2025年
過去日記について
アクセス統計
サーバ一覧
サイトの情報合計:
本日: